├── .gitattributes ├── .github ├── pull_request_template.md └── issue_template.md ├── .eslintignore ├── app ├── tray_icon.ico ├── tray_icon.png ├── tray_iconTemplate@2x.png ├── main │ ├── plugins │ │ ├── core │ │ │ ├── icon.png │ │ │ ├── index.js │ │ │ ├── plugins │ │ │ │ ├── getReadme.js │ │ │ │ ├── Preview │ │ │ │ │ ├── FormItem.js │ │ │ │ │ ├── ActionButton.js │ │ │ │ │ ├── Settings.js │ │ │ │ │ ├── styles.css │ │ │ │ │ └── index.js │ │ │ │ ├── getInstalledPlugins.js │ │ │ │ ├── format.js │ │ │ │ ├── loadPlugins.js │ │ │ │ ├── getAvailablePlugins.js │ │ │ │ ├── initializeAsync.js │ │ │ │ └── index.js │ │ │ ├── quit │ │ │ │ └── index.js │ │ │ ├── reload │ │ │ │ └── index.js │ │ │ ├── settings │ │ │ │ ├── Settings │ │ │ │ │ ├── styles.css │ │ │ │ │ ├── index.js │ │ │ │ │ ├── Hotkey.js │ │ │ │ │ └── countries.js │ │ │ │ └── index.js │ │ │ ├── autocomplete │ │ │ │ └── index.js │ │ │ └── version │ │ │ │ └── index.js │ │ ├── index.js │ │ └── externalPlugins.js │ ├── store │ │ ├── index.js │ │ ├── configureStore.js │ │ ├── configureStore.production.js │ │ └── configureStore.development.js │ ├── reducers │ │ ├── index.js │ │ └── search.js │ ├── createWindow │ │ ├── toggleWindow.js │ │ ├── showWindowWithTerm.js │ │ ├── handleUrl.js │ │ ├── checkForUpdates.js │ │ ├── donateDialog.js │ │ ├── AppTray.js │ │ └── buildMenu.js │ ├── containers │ │ ├── DevTools.js │ │ └── Search │ │ │ ├── styles.css │ │ │ └── index.js │ ├── constants │ │ ├── ui.js │ │ └── actionTypes.js │ ├── components │ │ ├── MainInput │ │ │ ├── styles.css │ │ │ └── index.js │ │ └── ResultsList │ │ │ ├── Row │ │ │ ├── index.js │ │ │ └── styles.css │ │ │ ├── styles.css │ │ │ └── index.js │ ├── css │ │ ├── global.css │ │ ├── themes │ │ │ ├── light.css │ │ │ └── dark.css │ │ └── system-font.css │ ├── index.html │ ├── main.js │ ├── actions │ │ └── search.js │ └── createWindow.js ├── lib │ ├── plugins │ │ ├── settings │ │ │ ├── get.js │ │ │ ├── index.js │ │ │ └── validate.js │ │ ├── index.js │ │ └── npm.js │ ├── rpc │ │ ├── functions │ │ │ ├── index.js │ │ │ └── initializePlugins.js │ │ ├── register.js │ │ ├── wrap.js │ │ └── events.js │ ├── loadThemes.js │ ├── initializePlugins.js │ ├── trackEvent.js │ ├── getWindowPosition.js │ └── config.js ├── background │ ├── createWindow.js │ ├── rpc │ │ └── initialize.js │ ├── background.js │ └── index.html ├── AppUpdater.js ├── package.json └── main.development.js ├── mocha-webpack.opts ├── postcss.config.js ├── test ├── .eslintrc └── actions │ └── search.spec.js ├── .vscode └── settings.json ├── .editorconfig ├── .babelrc ├── webpack.config.test.js ├── docs ├── plugins │ ├── styles.md │ ├── from-scratch.md │ ├── share.md │ ├── boilerplate.md │ ├── cerebro-tools.md │ ├── examples.md │ └── plugin-structure.md └── plugins.md ├── appveyor.yml ├── server.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── webpack.config.electron.js ├── .eslintrc ├── webpack.config.base.js ├── webpack.config.development.js ├── webpack.config.production.js ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | ./main.js 2 | webpack.config.*.js 3 | -------------------------------------------------------------------------------- /app/tray_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/cerebro/master/app/tray_icon.ico -------------------------------------------------------------------------------- /app/tray_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/cerebro/master/app/tray_icon.png -------------------------------------------------------------------------------- /app/tray_iconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/cerebro/master/app/tray_iconTemplate@2x.png -------------------------------------------------------------------------------- /app/main/plugins/core/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fede1024/cerebro/master/app/main/plugins/core/icon.png -------------------------------------------------------------------------------- /app/main/store/index.js: -------------------------------------------------------------------------------- 1 | import configureStore from './configureStore' 2 | 3 | export default configureStore() 4 | -------------------------------------------------------------------------------- /mocha-webpack.opts: -------------------------------------------------------------------------------- 1 | --colors 2 | --reporter spec 3 | --webpack-config webpack.config.test.js 4 | --exit 5 | test/**/*.spec.js 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-nested': {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/lib/plugins/settings/get.js: -------------------------------------------------------------------------------- 1 | import config from 'lib/config' 2 | 3 | export default (pluginName) => config.get('plugins')[pluginName] 4 | -------------------------------------------------------------------------------- /app/lib/plugins/settings/index.js: -------------------------------------------------------------------------------- 1 | import get from './get' 2 | import validate from './validate' 3 | 4 | export default { get, validate } 5 | -------------------------------------------------------------------------------- /app/lib/rpc/functions/index.js: -------------------------------------------------------------------------------- 1 | // Export RPC-versions of lib functions 2 | export { default as initializePlugins } from './initializePlugins' 3 | -------------------------------------------------------------------------------- /app/main/plugins/index.js: -------------------------------------------------------------------------------- 1 | import core from './core' 2 | import externalPlugins from './externalPlugins' 3 | 4 | export default Object.assign(externalPlugins, core) 5 | -------------------------------------------------------------------------------- /app/main/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import search from './search' 3 | 4 | const rootReducer = combineReducers({ 5 | search 6 | }) 7 | 8 | export default rootReducer 9 | -------------------------------------------------------------------------------- /app/background/createWindow.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | 3 | export default ({ src }) => { 4 | const backgroundWindow = new BrowserWindow({ 5 | show: false, 6 | }) 7 | backgroundWindow.loadURL(src) 8 | return backgroundWindow 9 | } 10 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "settings": { 6 | "import/core-modules": "electron", 7 | "import/resolver": { 8 | "webpack": { 9 | "config": "webpack.config.base.js" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/main/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'development') { 2 | module.exports = require('./configureStore.production') // eslint-disable-line global-require 3 | } else { 4 | module.exports = require('./configureStore.development') // eslint-disable-line global-require 5 | } 6 | -------------------------------------------------------------------------------- /app/main/createWindow/toggleWindow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show or hide main window 3 | * @return {BrowserWindow} appWindow 4 | */ 5 | export default (appWindow) => { 6 | if (appWindow.isVisible()) { 7 | appWindow.hide() 8 | } else { 9 | appWindow.show() 10 | appWindow.focus() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/main/plugins/core/index.js: -------------------------------------------------------------------------------- 1 | import autocomplete from './autocomplete' 2 | import quit from './quit' 3 | import plugins from './plugins' 4 | import settings from './settings' 5 | import version from './version' 6 | import reload from './reload' 7 | 8 | export default { 9 | autocomplete, quit, plugins, settings, version, reload 10 | } 11 | -------------------------------------------------------------------------------- /app/background/rpc/initialize.js: -------------------------------------------------------------------------------- 1 | import register from 'lib/rpc/register' 2 | 3 | import initializePlugins from 'lib/initializePlugins' 4 | 5 | /** 6 | * Register some functions. 7 | * After `register` function can be called using rpc from main window 8 | */ 9 | export default () => { 10 | register('initializePlugins', initializePlugins) 11 | } 12 | -------------------------------------------------------------------------------- /app/main/createWindow/showWindowWithTerm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show main window with updated search term 3 | * 4 | * @return {BrowserWindow} appWindow 5 | */ 6 | export default (appWindow, term) => { 7 | appWindow.show() 8 | appWindow.focus() 9 | appWindow.webContents.send('message', { 10 | message: 'showTerm', 11 | payload: term 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /app/main/store/configureStore.production.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | 5 | const enhancer = applyMiddleware(thunk) 6 | 7 | export default function configureStore(initialState) { 8 | return createStore(rootReducer, initialState, enhancer) 9 | } 10 | -------------------------------------------------------------------------------- /app/background/background.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import initializeRpc from './rpc/initialize' 4 | import { on } from 'lib/rpc/events' 5 | 6 | require('fix-path')() 7 | 8 | initializeRpc() 9 | 10 | // Handle `reload` rpc event and reload window 11 | on('reload', () => location.reload()) 12 | 13 | global.React = React 14 | global.ReactDOM = ReactDOM 15 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/getReadme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get plugin Readme.md content 3 | * 4 | * @param {String} repository Repository field from npm package 5 | * @return {Promise} 6 | */ 7 | export default (repo) => ( 8 | fetch(`https://api.github.com/repos/${repo}/readme`) 9 | .then(response => response.json()) 10 | .then(json => Buffer.from(json.content, 'base64').toString()) 11 | ) 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | ".git": true, 4 | ".eslintcache": true, 5 | "app/dist": true, 6 | "app/main.prod.js": true, 7 | "app/main.prod.js.map": true, 8 | "dll": true, 9 | "release": true, 10 | "node_modules": true, 11 | "npm-debug.log.*": true, 12 | "test/**/__snapshots__": true, 13 | "yarn.lock": true, 14 | ".tmp": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/main/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [.travis.yml] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /app/main/constants/ui.js: -------------------------------------------------------------------------------- 1 | // Height of main input 2 | export const INPUT_HEIGHT = 45 3 | 4 | // Heigth of default result line 5 | export const RESULT_HEIGHT = 45 6 | 7 | // Width of main window 8 | export const WINDOW_WIDTH = 650 9 | 10 | // Maximum results that would be rendered 11 | export const MAX_RESULTS = 25 12 | 13 | // Results view shows this count of resutls without scrollbar 14 | export const MIN_VISIBLE_RESULTS = 10 15 | -------------------------------------------------------------------------------- /app/lib/rpc/register.js: -------------------------------------------------------------------------------- 1 | import { on, send } from './events' 2 | 3 | /** 4 | * Register listener for rpc-calls. 5 | * @param {String} name Name of a function that is sent throuth messaging system 6 | * @param {Function} fn Function registered for this name 7 | */ 8 | export default (name, fn) => { 9 | on(`rpc.fn.${name}`, ({ args, callId }) => { 10 | fn.apply(null, args).then(result => send(callId, result)) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /app/main/components/MainInput/styles.css: -------------------------------------------------------------------------------- 1 | .input { 2 | width: 100%; 3 | height: 45px; 4 | background-color: var(--main-background-color); 5 | color: var(--main-font-color); 6 | font-size: 1.5em; 7 | border: 0; 8 | outline: none; 9 | padding: 0 10px; 10 | line-height: 60px; 11 | box-sizing: border-box; 12 | background: transparent; 13 | white-space: nowrap; 14 | -webkit-app-region: drag; 15 | -webkit-user-select: none; 16 | } 17 | -------------------------------------------------------------------------------- /app/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/main/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_TERM = 'UPDATE_TERM' 2 | export const MOVE_CURSOR = 'MOVE_CURSOR' 3 | export const SELECT_ELEMENT = 'SELECT_ELEMENT' 4 | export const SHOW_RESULT = 'SHOW_RESULT' 5 | export const HIDE_RESULT = 'HIDE_RESULT' 6 | export const UPDATE_RESULT = 'UPDATE_RESULT' 7 | 8 | export const RESET = 'RESET' 9 | export const CHANGE_VISIBLE_RESULTS = 'CHANGE_VISIBLE_RESULTS' 10 | 11 | export const ICON_LOADED = 'ICON_LOADED' 12 | -------------------------------------------------------------------------------- /app/main/containers/Search/styles.css: -------------------------------------------------------------------------------- 1 | .search { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | } 7 | 8 | .inputWrapper { 9 | position: relative; 10 | z-index: 2; 11 | width: 100%; 12 | height: 45px; 13 | } 14 | 15 | .autocomplete { 16 | position: absolute; 17 | z-index: 1; 18 | width: 100%; 19 | height: 45px; 20 | font-size: 1.5em; 21 | padding: 0 10px; 22 | line-height: 46px; 23 | box-sizing: border-box; 24 | color: var(--secondary-font-color); 25 | white-space: pre; 26 | } 27 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/Preview/FormItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Select, Text, Checkbox } from 'cerebro-ui/Form' 3 | 4 | const components = { 5 | bool: Checkbox, 6 | option: Select, 7 | } 8 | 9 | const FormItem = ({ type, ...props }) => { 10 | const Component = components[type] || Text 11 | 12 | return ( 13 | 14 | ) 15 | } 16 | 17 | FormItem.propTypes = { 18 | value: PropTypes.any, 19 | type: PropTypes.string.isRequired, 20 | } 21 | 22 | export default FormItem 23 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/getInstalledPlugins.js: -------------------------------------------------------------------------------- 1 | import { packageJsonPath } from 'lib/plugins' 2 | import { readFile } from 'fs' 3 | 4 | const readPackageJson = () => new Promise((resolve, reject) => { 5 | readFile(packageJsonPath, (err, source) => ( 6 | err ? reject(err) : resolve(source) 7 | )) 8 | }) 9 | 10 | /** 11 | * Get list of all installed plugins with versions 12 | * 13 | * @return {Promise} 14 | */ 15 | export default () => ( 16 | readPackageJson() 17 | .then(JSON.parse) 18 | .then(json => json.dependencies) 19 | ) 20 | -------------------------------------------------------------------------------- /app/lib/loadThemes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load all available themes 3 | * 4 | * @return {Array} Array of objects {value, label}. 5 | * Label is text that is shown in preferences theme selector 6 | */ 7 | export default () => { 8 | const prefix = process.env.HOT ? 'http://localhost:3000/' : '../' 9 | return [ 10 | { 11 | value: `${prefix}dist/main/css/themes/light.css`, 12 | label: 'Light' 13 | }, 14 | { 15 | value: `${prefix}dist/main/css/themes/dark.css`, 16 | label: 'Dark' 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /app/main/createWindow/handleUrl.js: -------------------------------------------------------------------------------- 1 | import showWindowWithTerm from './showWindowWithTerm' 2 | import { parse } from 'url' 3 | 4 | export default (mainWindow, url) => { 5 | const { host: action, query } = parse(url, { parseQueryString: true }) 6 | // Currently only search action supported. 7 | // We can extend this handler to support more 8 | // like `plugins/install` or do something plugin-related 9 | if (action === 'search') { 10 | showWindowWithTerm(mainWindow, query.term) 11 | } else { 12 | showWindowWithTerm(mainWindow, url) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/AppUpdater.js: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import { dialog, } from 'electron' 3 | import { autoUpdater } from "electron-updater"; 4 | 5 | const event = 'update-downloaded' 6 | 7 | export default class AppUpdater { 8 | constructor(w) { 9 | if (process.env.NODE_ENV === 'development' || os.platform() === "linux") { 10 | return 11 | } 12 | 13 | autoUpdater.on(event, (payload) => { 14 | w.webContents.send('message', { 15 | message: event, 16 | payload 17 | }) 18 | }) 19 | 20 | autoUpdater.checkForUpdates() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/lib/initializePlugins.js: -------------------------------------------------------------------------------- 1 | import plugins from '../main/plugins/' 2 | import { send } from 'lib/rpc/events' 3 | 4 | export default () => { 5 | // Run plugin initializers only when main window is loaded 6 | Object.keys(plugins).forEach(name => { 7 | const { initializeAsync } = plugins[name] 8 | if (!initializeAsync) return 9 | initializeAsync(data => { 10 | // Send message back to main window with initialization result 11 | send('plugin.message', { 12 | name, 13 | data, 14 | }) 15 | }) 16 | }) 17 | return Promise.resolve() 18 | } 19 | -------------------------------------------------------------------------------- /app/main/css/global.css: -------------------------------------------------------------------------------- 1 | @import "system-font.css"; 2 | @import "../../../node_modules/normalize.css/normalize.css"; 3 | @import "../../../node_modules/react-virtualized/styles.css"; 4 | @import "../../../node_modules/react-select/dist/react-select.css"; 5 | 6 | html, body { 7 | margin: 0; 8 | padding: 0; 9 | background-color: var(--main-background-color); 10 | color: var(--main-font-color); 11 | } 12 | 13 | body { 14 | position: relative; 15 | height: 100vh; 16 | font-family: var(--main-font); 17 | overflow-y: hidden; 18 | } 19 | 20 | #root { 21 | height: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react"], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "lodash", 6 | "babel-plugin-transform-decorators-legacy", 7 | "transform-es2015-classes" 8 | ], 9 | "env": { 10 | "production": { 11 | "plugins": [ 12 | "babel-plugin-transform-remove-debugger" 13 | ] 14 | }, 15 | "development": { 16 | "presets": ["react-hmre"] 17 | }, 18 | "test": { 19 | "plugins": [ 20 | "babel-plugin-add-module-exports", 21 | "babel-plugin-transform-es2015-modules-commonjs" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.test.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const config = require('./webpack.config.base') 3 | 4 | config.devtool = 'cheap-module-eval-source-map', 5 | config.target = 'electron-renderer' 6 | 7 | config.plugins = [ 8 | ...config.plugins, 9 | new webpack.LoaderOptionsPlugin({ 10 | debug: true 11 | }) 12 | ] 13 | 14 | config.module = Object.assign(config.module, { 15 | rules: Array.prototype.concat.call(config.module.rules, [ 16 | { 17 | test: /\.(css|svg|jpe?g|png)$/, 18 | use: 'null-loader' 19 | } 20 | ]) 21 | }) 22 | 23 | module.exports = config 24 | -------------------------------------------------------------------------------- /app/lib/plugins/settings/validate.js: -------------------------------------------------------------------------------- 1 | import { every } from 'lodash/fp' 2 | 3 | const VALID_TYPES = new Set([ 4 | 'string', 5 | 'number', 6 | 'bool', 7 | 'option', 8 | ]) 9 | 10 | const validSetting = ({ type, options }) => { 11 | // General validation of settings 12 | if (!type || !VALID_TYPES.has(type)) return false 13 | 14 | // Type-specific validations 15 | if (type === 'option') return Array.isArray(options) && options.length 16 | 17 | return true 18 | } 19 | 20 | export default ({ settings }) => { 21 | if (!settings) return true 22 | return every(validSetting)(settings) 23 | } 24 | -------------------------------------------------------------------------------- /docs/plugins/styles.md: -------------------------------------------------------------------------------- 1 | # Plugins → Styles for your plugin preview 2 | Currently if you want to reuse main app styles, you can use CSS variables from main themes ([light](../../app/main/css/themes/light.css), [dark](../../app/main/css/themes/dark.css)) 3 | 4 | > It is better to reuse css variables so custom themes can affect not only main app styles, but your plugin too. 5 | 6 | Example (reuse main border styles): 7 | 8 | ```css 9 | .item { 10 | border-bottom: var(--main-border); 11 | } 12 | ``` 13 | 14 | Later [cerebro-tools](./cerebro-tools.md) will include some reusable components. 15 | -------------------------------------------------------------------------------- /app/lib/trackEvent.js: -------------------------------------------------------------------------------- 1 | import ua from 'universal-analytics' 2 | import { machineIdSync } from 'node-machine-id' 3 | 4 | const DEFAULT_CATEGORY = 'Cerebro App' 5 | 6 | const trackingEnabled = process.env.NODE_ENV === 'production' 7 | let visitor 8 | 9 | try { 10 | visitor = ua('UA-87361302-1', machineIdSync(), { strictCidFormat: false }) 11 | } catch (err) { 12 | console.log('[machine-id error]', err) 13 | visitor = ua('UA-87361302-1') 14 | } 15 | 16 | export default ({ category, event, label, value }) => { 17 | if (trackingEnabled) { 18 | visitor.event(category || DEFAULT_CATEGORY, event, label, value).send() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/sindresorhus/appveyor-node/blob/master/appveyor.yml 2 | 3 | environment: 4 | matrix: 5 | - platform: x64 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | image: Visual Studio 2015 12 | 13 | init: 14 | - npm config set msvs_version 2015 # we need this to build `pty.js` 15 | 16 | install: 17 | - ps: Install-Product node 7 x64 18 | - set CI=true 19 | - npm install -g yarn 20 | - yarn 21 | 22 | build: off 23 | 24 | shallow_clone: true 25 | 26 | test_script: 27 | - node --version 28 | - yarn --version 29 | - yarn run lint 30 | - yarn run test 31 | 32 | on_success: 33 | - yarn run package 34 | -------------------------------------------------------------------------------- /app/main/plugins/core/quit/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | import { search } from 'cerebro-tools' 3 | import icon from '../icon.png' 4 | 5 | const KEYWORDS = [ 6 | 'Quit', 7 | 'Exit' 8 | ] 9 | 10 | const subtitle = 'Quit from Cerebro' 11 | const onSelect = () => remote.app.quit() 12 | 13 | /** 14 | * Plugin to exit from Cerebro 15 | * 16 | * @param {String} options.term 17 | * @param {Function} options.display 18 | */ 19 | const fn = ({ term, display }) => { 20 | const result = search(KEYWORDS, term).map(title => ({ 21 | icon, title, subtitle, onSelect, 22 | term: title, 23 | })) 24 | display(result) 25 | } 26 | 27 | export default { fn } 28 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/Preview/ActionButton.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { KeyboardNavItem } from 'cerebro-ui' 3 | 4 | const ActionButton = ({ action, onComplete, text }) => { 5 | const onSelect = () => { 6 | const timeout = new Promise(resolve => setTimeout(resolve, 1500)) 7 | Promise.all([action(), timeout]).then(onComplete) 8 | } 9 | return ( 10 | 11 | {text} 12 | 13 | ) 14 | } 15 | 16 | ActionButton.propTypes = { 17 | action: PropTypes.func.isRequired, 18 | text: PropTypes.string.isRequired, 19 | onComplete: PropTypes.func.isRequired, 20 | } 21 | 22 | export default ActionButton 23 | -------------------------------------------------------------------------------- /app/main/plugins/core/reload/index.js: -------------------------------------------------------------------------------- 1 | import { send } from 'lib/rpc/events' 2 | import icon from '../icon.png' 3 | 4 | const keyword = 'reload' 5 | const title = 'Reload' 6 | const subtitle = 'Reload Cerebro App' 7 | const onSelect = (event) => { 8 | send('reload') 9 | location.reload() 10 | event.preventDefault() 11 | } 12 | 13 | /** 14 | * Plugin to reload Cerebro 15 | * 16 | * @param {String} options.term 17 | * @param {Function} options.display 18 | */ 19 | const fn = ({ term, display }) => { 20 | const match = term.match(/^reload\s*/) 21 | 22 | if (match) { 23 | display({ icon, title, subtitle, onSelect }) 24 | } 25 | } 26 | 27 | export default { 28 | keyword, fn, icon, 29 | name: 'Reload' 30 | } 31 | -------------------------------------------------------------------------------- /app/lib/rpc/wrap.js: -------------------------------------------------------------------------------- 1 | import { once, send } from './events' 2 | 3 | /** 4 | * Generate uniq id for function call 5 | * @return {String} 6 | */ 7 | const generateId = () => { 8 | const now = new Date() 9 | return (+now + Math.random()).toString(16) 10 | } 11 | 12 | /** 13 | * Execute function by name in background. 14 | * 15 | * @param {String} name Registered name of function. 16 | * This name should be registered in `background/rpc/initialize` 17 | * @return {Function} Function that returns promise that is resolved 18 | * with result of registered function 19 | */ 20 | export default (name) => (...args) => ( 21 | new Promise(resolve => { 22 | const callId = generateId() 23 | once(callId, resolve) 24 | send(`rpc.fn.${name}`, { args, callId }) 25 | }) 26 | ) 27 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/format.js: -------------------------------------------------------------------------------- 1 | import { flow, lowerCase, words, capitalize, trim, map, join } from 'lodash/fp' 2 | 3 | /** 4 | * Remove unnecessary information from plugin name or description 5 | * like `Cerebro plugin for` 6 | * @param {String} str 7 | * @return {String} 8 | */ 9 | const removeNoise = (str) => ( 10 | str.replace(/^cerebro\s?(plugin)?\s?(to|for)?/i, '') 11 | ) 12 | 13 | export const name = flow( 14 | lowerCase, 15 | removeNoise, 16 | trim, 17 | words, 18 | map(capitalize), 19 | join(' ') 20 | ) 21 | 22 | export const description = flow( 23 | removeNoise, 24 | trim, 25 | capitalize, 26 | ) 27 | 28 | export const version = (plugin) => ( 29 | plugin.isUpdateAvailable ? 30 | `${plugin.installedVersion} → ${plugin.version}` : 31 | plugin.version 32 | ) 33 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerebro", 3 | "productName": "Cerebro", 4 | "description": "Extendable electron-based open-source Spolight and Alfred analogue", 5 | "version": "0.2.8", 6 | "main": "./main.js", 7 | "license": "MIT", 8 | "author": { 9 | "name": "Alexandr Subbotin", 10 | "email": "kelionweb@gmail.com", 11 | "url": "https://github.com/KELiON" 12 | }, 13 | "scripts": { 14 | "rebuild": "npm rebuild --runtime=electron --target=1.6.10 --disturl=https://atom.io/download/electron --abi=53" 15 | }, 16 | "dependencies": { 17 | "chokidar": "^1.6.1", 18 | "electron-updater": "^1.15.0", 19 | "nodobjc": "^2.1.0", 20 | "rmdir": "^1.2.0", 21 | "semver": "^5.3.0", 22 | "universal-analytics": "^0.4.8" 23 | }, 24 | "optionalDependencies": {}, 25 | "devDependencies": {} 26 | } 27 | -------------------------------------------------------------------------------- /docs/plugins/from-scratch.md: -------------------------------------------------------------------------------- 1 | # Plugins → Create from scratch 2 | 1. Create new directory and run `npm init` in your folder. Enter name, version, description and entry point for your plugin. 3 | 1. Create entry point script, i.e. `index.js` 4 | 1. Your plugin should be moved to `~/Library/Application Support/Cerebro/plugins/node_modules`. Easiest way to it is to create symlink. I.e. you can use this command from your plugin directory: `ln -s "${PWD}" "${HOME}/Library/Application Support/Cerebro/plugins/node_modules/${PWD##*/}"` 5 | 1. Open Cerebro settings and turn on developer mode; 6 | 1. You are ready to go! Use cmd+alt+i to open developer tools in Cerebro and cmd+r from developer tools to reload Cerebro so you can see you plugin changes. 7 | 8 | Now you can check [how to write plugin](./plugin-structure.md). 9 | -------------------------------------------------------------------------------- /app/main/plugins/core/settings/Settings/styles.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | display: flex; 3 | align-self: flex-start; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .label { 9 | margin-right: 15px; 10 | margin-top: 8px; 11 | min-width: 60px; 12 | max-width: 60px; 13 | } 14 | 15 | .checkbox { 16 | margin-right: 5px; 17 | } 18 | 19 | .settingItem { 20 | padding: 20px; 21 | box-sizing: border-box; 22 | width: 100%; 23 | border-color: #d9d9d9 #ccc #b3b3b3; 24 | border-top: 1px solid #ccc; 25 | margin-top: 16px; 26 | } 27 | 28 | .header { 29 | font-weight: bold; 30 | } 31 | 32 | .input { 33 | font-size: 16px; 34 | line-height: 34px; 35 | padding: 0 10px; 36 | box-sizing: border-box; 37 | width: 100%; 38 | border-color: #d9d9d9 #ccc #b3b3b3; 39 | border-radius: 4px; 40 | border: 1px solid #ccc; 41 | } 42 | -------------------------------------------------------------------------------- /app/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cerebro 6 | 7 | 17 | 18 | 19 |
20 |
21 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/lib/rpc/events.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | import EventEmitter from 'events' 3 | 4 | const emitter = new EventEmitter() 5 | 6 | /** 7 | * Channel name that is managed by main process. 8 | * @type {String} 9 | */ 10 | const CHANNEL = 'message' 11 | 12 | // Start listening for rpc channel 13 | ipcRenderer.on(CHANNEL, (_, { message, payload }) => { 14 | console.log(`[rpc] emit ${message}`) 15 | emitter.emit(message, payload) 16 | }) 17 | 18 | /** 19 | * Send message to rpc-channel 20 | * @param {String} message 21 | * @param {} payload 22 | */ 23 | export const send = (message, payload) => { 24 | console.log(`[rpc] send ${message}`) 25 | ipcRenderer.send(CHANNEL, { 26 | message, 27 | payload 28 | }) 29 | } 30 | 31 | export const on = emitter.on.bind(emitter) 32 | export const off = emitter.removeListener.bind(emitter) 33 | export const once = emitter.once.bind(emitter) 34 | -------------------------------------------------------------------------------- /app/main/components/MainInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import styles from './styles.css' 3 | 4 | class MainInput extends Component { 5 | focus() { 6 | this.refs.input.focus() 7 | } 8 | render() { 9 | return ( 10 | this.props.onChange(e.target.value)} 18 | onKeyDown={this.props.onKeyDown} 19 | onFocus={this.props.onFocus} 20 | onBlur={this.props.onBlur} 21 | /> 22 | ) 23 | } 24 | } 25 | 26 | MainInput.propTypes = { 27 | value: PropTypes.string, 28 | onChange: PropTypes.func, 29 | onKeyDown: PropTypes.func, 30 | onFocus: PropTypes.func, 31 | onBlur: PropTypes.func, 32 | } 33 | 34 | export default MainInput 35 | -------------------------------------------------------------------------------- /app/main/css/themes/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Main fonts and colors */ 3 | --main-background-color: rgba(255, 255, 255, 1); 4 | --main-font: system; 5 | --main-font-color: #000000; 6 | 7 | /* border styles */ 8 | --main-border: 1px solid #eee; 9 | 10 | /* Secondary fonts and colors */ 11 | --secondary-font-color: #999; 12 | 13 | /* results list */ 14 | --result-background: transparent; 15 | --result-title-color: var(--main-font-color); 16 | --result-subtitle-color: #cccccc; 17 | 18 | /* selected result */ 19 | --selected-result-title-color: white; 20 | --selected-result-subtitle-color: var(--result-subtitle-color); 21 | --selected-result-background: rgba(18, 110, 219, 1); 22 | 23 | /* inputs */ 24 | --preview-input-background: white; 25 | --preview-input-color: var(--main-font-color); 26 | --preview-input-border: 1px solid #ccc; 27 | 28 | /* filter for previews */ 29 | --preview-filter: none; 30 | } 31 | -------------------------------------------------------------------------------- /docs/plugins/share.md: -------------------------------------------------------------------------------- 1 | # Plugins → Share 2 | When your plugin is ready, you can share it with all Cerebro users so they can find and install it using `plugins` command in Cerebro. 3 | 4 | All you need is to publish your module to npm. Just run from your plugin folder: 5 | 6 | ``` 7 | npm publish ./ 8 | ``` 9 | 10 | If you have any problems check out [publishing packages](https://docs.npmjs.com/getting-started/publishing-npm-packages) in npm documentation 11 | 12 | ## Checklist 13 | 1. Update your repository `Readme.md`, add screenshot or gif; 14 | 1. Push your plugin to open github repository – this repository is used by cerebro, at least to show `Readme.md` of your plugin; 15 | 1. Make sure that you have changed package.json metadata: module name, description, author and link to github repository; 16 | 1. Add `cerebro-plugin` keyword to package.json keywords section. Otherwise your plugin won't be shown in Cerebro; 17 | -------------------------------------------------------------------------------- /app/main/css/themes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Main fonts and colors */ 3 | --main-background-color: rgba(62, 65, 67, 1); 4 | --main-font: system; 5 | --main-font-color: white; 6 | 7 | /* border styles */ 8 | --main-border: 1px solid #686869; 9 | 10 | /* Secondary fonts and colors */ 11 | --secondary-font-color: #9B9D9F; 12 | 13 | /* results list */ 14 | --result-background: transparent; 15 | --result-title-color: var(--main-font-color); 16 | --result-subtitle-color: #cccccc; 17 | 18 | /* selected result */ 19 | --selected-result-title-color: white; 20 | --selected-result-subtitle-color: var(--result-subtitle-color); 21 | --selected-result-background: #1972D6; 22 | 23 | /* inputs */ 24 | --preview-input-background: #2E2E2C; 25 | --preview-input-color: var(--main-font-color); 26 | --preview-input-border: 0; 27 | 28 | /* filter for previews */ 29 | --preview-filter: invert(100%) hue-rotate(180deg) contrast(80%);; 30 | } 31 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Cerebro plugin is just a javascript module. All you need is to write a function, that takes one object and call a function from arguments with your results. 4 | 5 | > You should have installed node and npm. If you don't have it: follow [this](https://docs.npmjs.com/getting-started/installing-node) instructions. 6 | 7 | You can create your plugin [from scratch](./plugins/from-scratch.md), but I'd suggest you to use [boilerplate](./plugins/boilerplate.md). In this case you can focus on source of your plugin, not on tools around it. 8 | 9 | Links: 10 | * [Create plugin from boilerplate](./plugins/boilerplate.md); 11 | * [Plugin structure](./plugins/plugin-structure.md); 12 | * [Cerebro tools](./plugins/cerebro-tools.md); 13 | * [Styles](./plugins/styles.md) for you plugin previews; 14 | * [Share your plugin](./plugins/share.md); 15 | * [Examples](./plugins/examples.md); 16 | * [Create plugin from scratch](./plugins/from-scratch.md). 17 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/loadPlugins.js: -------------------------------------------------------------------------------- 1 | import { memoize } from 'cerebro-tools' 2 | import availablePlugins from './getAvailablePlugins' 3 | import getInstalledPlugins from './getInstalledPlugins' 4 | import semver from 'semver' 5 | 6 | const maxAge = 5 * 60 * 1000 7 | 8 | const getAvailablePlugins = memoize(availablePlugins, { maxAge }) 9 | 10 | const parseVersion = (version) => ( 11 | semver.valid((version || '').replace(/^\^/, '')) || '0.0.0' 12 | ) 13 | 14 | export default () => ( 15 | Promise.all([ 16 | getAvailablePlugins(), 17 | getInstalledPlugins() 18 | ]).then(([available, installed]) => available.map(plugin => { 19 | const installedVersion = parseVersion(installed[plugin.name]) 20 | const isInstalled = !!installed[plugin.name] 21 | const isUpdateAvailable = isInstalled && semver.gt(plugin.version, installedVersion) 22 | return { 23 | ...plugin, 24 | installedVersion, 25 | isInstalled, 26 | isUpdateAvailable 27 | } 28 | })) 29 | ) 30 | -------------------------------------------------------------------------------- /app/main/store/configureStore.development.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { persistState } from 'redux-devtools' 3 | import thunk from 'redux-thunk' 4 | import createLogger from 'redux-logger' 5 | import rootReducer from '../reducers' 6 | import DevTools from '../containers/DevTools' 7 | 8 | const logger = createLogger({ 9 | level: 'info', 10 | collapsed: true, 11 | }) 12 | 13 | 14 | const enhancer = compose( 15 | applyMiddleware(thunk, logger), 16 | DevTools.instrument(), 17 | persistState( 18 | window.location.href.match( 19 | /[?&]debug_session=([^&]+)\b/ 20 | ) 21 | ) 22 | ) 23 | 24 | export default function configureStore(initialState) { 25 | const store = createStore(rootReducer, initialState, enhancer) 26 | 27 | if (module.hot) { 28 | module.hot.accept('../reducers', () => 29 | store.replaceReducer(require('../reducers')) // eslint-disable-line global-require 30 | ) 31 | } 32 | 33 | return store 34 | } 35 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const express = require('express') 4 | const webpack = require('webpack') 5 | const webpackDevMiddleware = require('webpack-dev-middleware') 6 | const webpackHotMiddleware = require('webpack-hot-middleware') 7 | 8 | const config = require('./webpack.config.development') 9 | 10 | const app = express() 11 | const compiler = webpack(config) 12 | const PORT = 3000 13 | 14 | const wdm = webpackDevMiddleware(compiler, { 15 | publicPath: config.output.publicPath, 16 | stats: { 17 | colors: true 18 | } 19 | }) 20 | 21 | app.use(wdm) 22 | 23 | app.use(webpackHotMiddleware(compiler)) 24 | 25 | const server = app.listen(PORT, 'localhost', err => { 26 | if (err) { 27 | console.error(err) 28 | return 29 | } 30 | 31 | console.log(`Listening at http://localhost:${PORT}`) 32 | }) 33 | 34 | process.on('SIGTERM', () => { 35 | console.log('Stopping dev server') 36 | wdm.close() 37 | server.close(() => { 38 | process.exit(0) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Tern-js work files 20 | .tern-port 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | 32 | # Folder view configuration files 33 | .DS_Store 34 | Desktop.ini 35 | 36 | # Thumbnail cache files 37 | ._* 38 | Thumbs.db 39 | 40 | # App packaged 41 | dist 42 | release 43 | /app/main.js 44 | /app/main.js.map 45 | 46 | .tmp/ 47 | 48 | # IDEs 49 | .idea 50 | 51 | *.sublime-project 52 | *.sublime-workspace -------------------------------------------------------------------------------- /app/lib/rpc/functions/initializePlugins.js: -------------------------------------------------------------------------------- 1 | import wrap from '../wrap' 2 | import plugins from 'main/plugins/' 3 | import { on } from 'lib/rpc/events' 4 | 5 | const asyncInitializePlugins = wrap('initializePlugins') 6 | 7 | const initializePlugins = () => { 8 | // Call background initializers 9 | asyncInitializePlugins() 10 | 11 | // Call foreground initializers 12 | Object.keys(plugins).forEach(name => { 13 | const { initialize } = plugins[name] 14 | if (initialize) { 15 | // Sync plugin initialization 16 | try { 17 | initialize() 18 | } catch (e) { 19 | console.error(`Failed to initialize plugin: ${name}`, e) 20 | } 21 | } 22 | }) 23 | } 24 | 25 | /** 26 | * RPC-call for plugins initializations 27 | */ 28 | export default () => { 29 | // Start listening for replies from plugin async initializers 30 | on('plugin.message', ({ name, data }) => { 31 | const plugin = plugins[name] 32 | if (plugin.onMessage) plugin.onMessage(data) 33 | }) 34 | 35 | return initializePlugins() 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: node_js 5 | 6 | branches: 7 | only: 8 | - master 9 | 10 | matrix: 11 | include: 12 | - os: linux 13 | node_js: 7 14 | env: CC=clang CXX=clang++ npm_config_clang=1 15 | compiler: clang 16 | 17 | addons: 18 | apt: 19 | packages: 20 | - libgnome-keyring-dev 21 | - libicns 22 | - graphicsmagick 23 | - xz-utils 24 | - rpm 25 | - bsdtar 26 | 27 | before_install: 28 | - npm install -g yarn 29 | 30 | cache: yarn 31 | 32 | addons: 33 | apt: 34 | sources: 35 | - ubuntu-toolchain-r-test 36 | packages: 37 | - g++-6 38 | 39 | install: 40 | - export CXX="g++-6" 41 | - yarn 42 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" 43 | 44 | before_script: 45 | - export DISPLAY=:99.0 46 | - sh -e /etc/init.d/xvfb start & 47 | - sleep 3 48 | 49 | script: 50 | - yarn run lint 51 | - yarn run test 52 | - yarn run package 53 | -------------------------------------------------------------------------------- /app/lib/plugins/index.js: -------------------------------------------------------------------------------- 1 | import { app, remote } from 'electron' 2 | import path from 'path' 3 | import npm from './npm' 4 | import fs from 'fs' 5 | 6 | const ensureFile = (src, content = '') => { 7 | if (!fs.existsSync(src)) { 8 | fs.writeFileSync(src, content) 9 | } 10 | } 11 | 12 | const ensureDir = (src) => { 13 | if (!fs.existsSync(src)) { 14 | fs.mkdirSync(src) 15 | } 16 | } 17 | 18 | const EMPTY_PACKAGE_JSON = JSON.stringify({ 19 | name: 'cerebro-plugins', 20 | dependencies: {} 21 | }, null, 2) 22 | 23 | const electronApp = remote ? remote.app : app 24 | export const pluginsPath = path.join(electronApp.getPath('userData'), 'plugins') 25 | export const modulesDirectory = path.join(pluginsPath, 'node_modules') 26 | export const packageJsonPath = path.join(pluginsPath, 'package.json') 27 | 28 | export const ensureFiles = () => { 29 | ensureDir(pluginsPath) 30 | ensureDir(modulesDirectory) 31 | ensureFile(packageJsonPath, EMPTY_PACKAGE_JSON) 32 | } 33 | 34 | export const client = npm(pluginsPath) 35 | export { default as settings } from './settings' 36 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/getAvailablePlugins.js: -------------------------------------------------------------------------------- 1 | import { flow, map, sortBy } from 'lodash/fp' 2 | 3 | /** 4 | * API endpoint to search all cerebro plugins 5 | * @type {String} 6 | */ 7 | // TODO: after ending beta-testing of new version – change back to registry.npmjs.com 8 | // const URL = 'https://registry.npmjs.com/-/v1/search?from=0&size=500&text=keywords:cerebro-plugin' 9 | const URL = 'https://api.npms.io/v2/search?from=0&size=250&q=keywords%3Acerebro-plugin,cerebro-extracted-plugin' 10 | 11 | /** 12 | * Get all available plugins for Cerebro 13 | * @return {Promise} 14 | */ 15 | export default () => ( 16 | fetch(URL) 17 | .then(response => response.json()) 18 | .then(json => flow( 19 | sortBy(p => -p.score.detail.popularity), 20 | map(p => ({ 21 | name: p.package.name, 22 | version: p.package.version, 23 | description: p.package.description, 24 | homepage: p.package.links.homepage, 25 | repo: p.package.links.repository 26 | })) 27 | )(json.results) 28 | ) 29 | ) 30 | -------------------------------------------------------------------------------- /app/main/plugins/core/autocomplete/index.js: -------------------------------------------------------------------------------- 1 | import { search } from 'cerebro-tools' 2 | import plugins from '../../index' 3 | import { flow, filter, map, partialRight, values } from 'lodash/fp' 4 | 5 | const toString = plugin => plugin.keyword 6 | const notMatch = term => plugin => ( 7 | plugin.keyword !== term && `${plugin.keyword} ` !== term 8 | ) 9 | 10 | const pluginToResult = actions => res => ({ 11 | title: res.name, 12 | icon: res.icon, 13 | term: `${res.keyword} `, 14 | onSelect: (event) => { 15 | event.preventDefault() 16 | actions.replaceTerm(`${res.keyword} `) 17 | } 18 | }) 19 | 20 | /** 21 | * Plugin for autocomplete other plugins 22 | * 23 | * @param {String} options.term 24 | * @param {Function} options.display 25 | */ 26 | const fn = ({ term, display, actions }) => flow( 27 | values, 28 | filter(plugin => !!plugin.keyword), 29 | partialRight(search, [term, toString]), 30 | filter(notMatch(term)), 31 | map(pluginToResult(actions)), 32 | display 33 | )(plugins) 34 | 35 | export default { 36 | fn, 37 | name: 'Plugins autocomplete', 38 | } 39 | -------------------------------------------------------------------------------- /docs/plugins/boilerplate.md: -------------------------------------------------------------------------------- 1 | # Plugins → Create using boilerplate 2 | 1. [Download latest version](https://github.com/KELiON/cerebro-plugin/archive/master.zip) of [cerebro-plugin](https://github.com/KELiON/cerebro-plugin) boilerplate and unarchive it to some folder; 3 | 1. Run `npm install` inside this folder; 4 | 1. Open Cerebro settings and turn on developer mode; 5 | 1. run `npm run debug` – it will create symlink to your current folder in cerebro plugins directory and start webpack in watch mode, so you don't need to rebuild you source code everytime. On windows use [cygwin](https://www.cygwin.com/) or [git bash](https://git-scm.com/download/win) instead of cmd.exe and `./scripts/debug` command instead of `npm run debug`. If you choose to use cmd.exe, use `npm run debug:windows`. See [cerebro-plugin](https://github.com/KELiON/cerebro-plugin). 6 | 1. You are ready to go! Use cmd+alt+i to open developer tools in Cerebro and cmd+r from developer tools to reload Cerebro so you can see you plugin changes. 7 | 8 | Now you can check [how to write plugin](./plugin-structure.md). 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Alexandr Subbotin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/main/plugins/core/version/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { search } from 'cerebro-tools' 3 | import icon from '../icon.png' 4 | import { version } from '../../../../../package.json' 5 | 6 | // Settings plugin name 7 | const NAME = 'Cerebro Version' 8 | 9 | // Settings plugins in the end of list 10 | const order = 9 11 | 12 | // Phrases that used to find settings plugins 13 | const KEYWORDS = [ 14 | NAME, 15 | 'ver', 16 | 'version' 17 | ] 18 | 19 | /** 20 | * Plugin to show app settings in results list 21 | * 22 | * @param {String} options.term 23 | * @param {Function} options.display 24 | */ 25 | const versionPlugin = ({ term, display, actions }) => { 26 | const found = search(KEYWORDS, term).length > 0 27 | 28 | if (found) { 29 | const results = [{ 30 | order, 31 | icon, 32 | title: NAME, 33 | term: NAME, 34 | getPreview: () => (
{version}
), 35 | onSelect: (event) => { 36 | event.preventDefault() 37 | actions.replaceTerm(NAME) 38 | } 39 | }] 40 | display(results) 41 | } 42 | } 43 | 44 | export default { 45 | name: NAME, 46 | fn: versionPlugin 47 | } 48 | -------------------------------------------------------------------------------- /app/main/components/ResultsList/Row/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { SmartIcon } from 'cerebro-ui' 3 | import styles from './styles.css' 4 | 5 | class Row extends Component { 6 | classNames() { 7 | return [ 8 | styles.row, 9 | this.props.selected ? styles.selected : null 10 | ].join(' ') 11 | } 12 | renderIcon() { 13 | const { icon } = this.props 14 | if (!icon) return null 15 | return 16 | } 17 | render() { 18 | const { 19 | title, 20 | onSelect, 21 | onMouseMove, 22 | subtitle 23 | } = this.props 24 | return ( 25 |
26 | {this.renderIcon()} 27 |
28 | {title &&
{title}
} 29 | {subtitle &&
{subtitle}
} 30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | Row.propTypes = { 37 | title: PropTypes.string, 38 | icon: PropTypes.string, 39 | selected: PropTypes.bool, 40 | subtitle: PropTypes.string, 41 | onSelect: PropTypes.func, 42 | onMouseMove: PropTypes.func, 43 | } 44 | 45 | export default Row 46 | -------------------------------------------------------------------------------- /app/main/plugins/core/settings/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { search } from 'cerebro-tools' 3 | import Settings from './Settings' 4 | import icon from '../icon.png' 5 | 6 | // Settings plugin name 7 | const NAME = 'Cerebro Settings' 8 | 9 | // Settings plugins in the end of list 10 | const order = 9 11 | 12 | // Phrases that used to find settings plugins 13 | const KEYWORDS = [ 14 | NAME, 15 | 'Cerebro Preferences', 16 | 'cfg', 17 | 'config', 18 | 'params' 19 | ] 20 | 21 | /** 22 | * Plugin to show app settings in results list 23 | * 24 | * @param {String} options.term 25 | * @param {Function} options.display 26 | */ 27 | const settingsPlugin = ({ term, display, config, actions }) => { 28 | const found = search(KEYWORDS, term).length > 0 29 | if (found) { 30 | const results = [{ 31 | order, 32 | icon, 33 | title: NAME, 34 | term: NAME, 35 | getPreview: () => ( 36 | config.set(key, value)} 38 | get={(key) => config.get(key)} 39 | /> 40 | ), 41 | onSelect: (event) => { 42 | event.preventDefault() 43 | actions.replaceTerm(NAME) 44 | } 45 | }] 46 | display(results) 47 | } 48 | } 49 | 50 | export default { 51 | name: NAME, 52 | fn: settingsPlugin 53 | } 54 | -------------------------------------------------------------------------------- /webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const baseConfig = require('./webpack.config.base'); 3 | 4 | const isProduction = process.env.NODE_ENV === 'production'; 5 | 6 | const plugins = [ 7 | new webpack.DefinePlugin({ 8 | 'process.env': { 9 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 10 | } 11 | }) 12 | ]; 13 | 14 | module.exports = { 15 | ...baseConfig, 16 | module: { 17 | rules: [{ 18 | test: /\.jsx?$/, 19 | exclude: /(node_modules)/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | plugins: [ 24 | "babel-plugin-transform-object-rest-spread", 25 | "babel-plugin-lodash", 26 | "babel-plugin-add-module-exports", 27 | "babel-plugin-transform-decorators-legacy", 28 | "babel-plugin-transform-es2015-classes", 29 | "babel-plugin-transform-es2015-modules-commonjs" 30 | ] 31 | } 32 | } 33 | }] 34 | }, 35 | 36 | plugins, 37 | 38 | devtool: 'source-map', 39 | entry: './app/main.development', 40 | 41 | output: { 42 | ...baseConfig.output, 43 | path: __dirname, 44 | filename: './app/main.js' 45 | }, 46 | 47 | target: 'electron-main', 48 | 49 | node: { 50 | __dirname: false, 51 | __filename: false 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | - [ ] I am on the [latest](https://github.com/KELiON/cerebro/releases/latest) Cerebro.app version 11 | - [ ] I have searched the [issues](https://github.com/KELiON/cerebro/issues) of this repo and believe that this is not a duplicate 12 | 13 | 22 | 23 | - **OS version and name**: 24 | - **Cerebro.app version**: 25 | - **Relevant information from devtools** _(See above how to open it)_: 26 | 27 | ## Issue 28 | 29 | -------------------------------------------------------------------------------- /docs/plugins/cerebro-tools.md: -------------------------------------------------------------------------------- 1 | # Plugins → `cerebro-tools` 2 | `cerebro-tools` is a package to DRY your plugins code. Currently it includes only three functions: `memoize`, `search` and `shellCommand`. 3 | 4 | > Later more functions and react components will be added. 5 | 6 | ## Search 7 | Use this function to filter your collection by provided search term. 8 | ```js 9 | // Require from npm module 10 | const { search } = require('cerebro-tools'); 11 | 12 | // Convert item from collection to string. Default is `(el) => el` 13 | const toString = (el) => el.title; 14 | 15 | // Search for `term` in your collection 16 | const results = search(collection, term, toString); 17 | ``` 18 | 19 | ## Memoize 20 | Use this function to reduce calls to external APIs. 21 | 22 | ```js 23 | const { memoize } = require('cerebro-tools'); 24 | 25 | const fetchResults = require('path/to/fetchResults'); 26 | 27 | const cachedFetchResults = memoize(fetchResults) 28 | ``` 29 | 30 | Check out [memoizee](https://github.com/medikoo/memoizee) docs for more info. 31 | 32 | How to [share your plugin](./share.md). 33 | 34 | ## shellCommand 35 | 36 | It is just promise-wrapper for `require('child_process').exec`: 37 | 38 | ``` 39 | const { shellCommand } = require('cerebro-tools'); 40 | 41 | const result = shellCommand('ls ./').then(output => console.log(output)); 42 | ``` 43 | -------------------------------------------------------------------------------- /app/main/components/ResultsList/Row/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO: colors should be moved to variables 3 | */ 4 | .row { 5 | position: relative; 6 | display: flex; 7 | flex-wrap: nowrap; 8 | flex-direction: row; 9 | align-items: flex-start; 10 | white-space: nowrap; 11 | width: 100%; 12 | cursor: pointer; 13 | box-sizing: border-box; 14 | height: 45px; 15 | padding: 3px 5px; 16 | align-items: center; 17 | color: var(--main-font-color); 18 | background: var(--result-background); 19 | } 20 | 21 | .icon { 22 | max-height: 30px; 23 | max-width: 30px; 24 | margin-right: 5px; 25 | } 26 | 27 | .title { 28 | font-size: .8em; 29 | max-width: 100%; 30 | overflow-x: hidden; 31 | color: var(--result-title-color); 32 | } 33 | 34 | 35 | .subtitle { 36 | font-size: 0.8em; 37 | font-weight: 300; 38 | color: var(--result-subtitle-color); 39 | max-width: 100%; 40 | overflow-x: hidden; 41 | } 42 | 43 | .selected { 44 | background: var(--selected-result-background); 45 | .title { 46 | color: var(--selected-result-title-color); 47 | } 48 | .subtitle { 49 | color: var(--selected-result-subtitle-color); 50 | } 51 | } 52 | 53 | .details { 54 | position: relative; 55 | display: flex; 56 | flex-grow: 2; 57 | flex-wrap: wrap; 58 | flex-direction: column; 59 | align-items: flex-start; 60 | justify-content: center; 61 | height: 90%; 62 | } 63 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "consistent-return": 0, 11 | "comma-dangle": 0, 12 | "no-use-before-define": 0, 13 | "no-console": 0, 14 | "semi": ["error", "never"], 15 | "no-confusing-arrow": ["off"], 16 | "no-useless-escape": 0, 17 | "no-mixed-operators": "off", 18 | "no-continue": "off", 19 | "import/no-extraneous-dependencies": "off", 20 | "import/imports-first": "off", 21 | "import/extensions": "off", 22 | "react/jsx-no-bind": 0, 23 | "react/prefer-stateless-function": 0, 24 | "react/no-string-refs": "off", 25 | "react/forbid-prop-types": "off", 26 | "react/no-unused-prop-types": "off", 27 | "react/no-danger": "off", 28 | "react/jsx-filename-extension": "off", 29 | "react/no-unescaped-entities": "off", 30 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], 31 | "prefer-spread": "off", 32 | "class-methods-use-this": "off", 33 | "jsx-a11y/no-static-element-interactions": "off", 34 | "jsx-a11y/label-has-for": "off" 35 | }, 36 | "plugins": [ 37 | "import", 38 | "react" 39 | ], 40 | "settings": { 41 | "import/core-modules": "electron", 42 | "import/resolver": { 43 | "webpack": { 44 | "config": "../webpack.config.base.js" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); 5 | 6 | // all dependecies from app/package.json will be included in build/node_modules 7 | const externals = Object.assign( 8 | require('./app/package.json').dependencies, 9 | require('./app/package.json').optionalDependencies 10 | ); 11 | 12 | module.exports = { 13 | module: { 14 | rules: [{ 15 | test: /\.jsx?$/, 16 | use: 'babel-loader', 17 | exclude: (modulePath) => ( 18 | modulePath.match(/node_modules/) && !modulePath.match(/node_modules(\/|\\)cerebro-ui/) 19 | ) 20 | }, { 21 | test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$|\.wav$|\.mp3$/, 22 | use: ['url-loader'] 23 | }] 24 | }, 25 | output: { 26 | path: path.join(__dirname, 'app'), 27 | filename: '[name].bundle.js', 28 | libraryTarget: 'commonjs2' 29 | }, 30 | resolve: { 31 | modules: [ 32 | path.join(__dirname, "app"), 33 | "node_modules" 34 | ], 35 | extensions: ['.js'], 36 | }, 37 | plugins: [ 38 | new LodashModuleReplacementPlugin(), 39 | new webpack.DefinePlugin({ 40 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 41 | }), 42 | new CopyWebpackPlugin([{ 43 | from: 'app/main/css/themes/*', 44 | to: './main/css/themes/[name].[ext]' 45 | }]) 46 | ], 47 | externals: Object.keys(externals || {}) 48 | }; 49 | -------------------------------------------------------------------------------- /app/main/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import store from './store' 5 | import Search from './containers/Search' 6 | import './css/global.css' 7 | import { initializePlugins } from 'lib/rpc/functions' 8 | import { on } from 'lib/rpc/events' 9 | import { updateTerm } from './actions/search' 10 | import config from '../lib/config' 11 | 12 | require('fix-path')() 13 | 14 | global.React = React 15 | global.ReactDOM = ReactDOM 16 | 17 | /** 18 | * Change current theme 19 | * 20 | * @param {String} src Absolute path to new theme css file 21 | */ 22 | const changeTheme = (src) => { 23 | document.getElementById('cerebro-theme').href = src 24 | } 25 | 26 | // Set theme from config 27 | changeTheme(config.get('theme')) 28 | 29 | // Render main container 30 | ReactDOM.render( 31 | 32 | 33 | , 34 | document.getElementById('root') 35 | ) 36 | 37 | // Initialize plugins 38 | initializePlugins() 39 | 40 | // Handle `showTerm` rpc event and replace search term with payload 41 | on('showTerm', (term) => store.dispatch(updateTerm(term))) 42 | 43 | on('update-downloaded', () => ( 44 | new Notification('Cerebro: update is ready to install', { 45 | body: 'New version is downloaded and will be automatically installed on quit' 46 | }) 47 | )) 48 | 49 | // Handle `updateTheme` rpc event and change current theme 50 | on('updateTheme', changeTheme) 51 | 52 | // Handle `reload` rpc event and reload window 53 | on('reload', () => location.reload()) 54 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/Preview/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import config from 'lib/config' 3 | import FormItem from './FormItem' 4 | import styles from './styles.css' 5 | 6 | export default class Settings extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { 10 | values: config.get('plugins')[props.name], 11 | } 12 | this.renderSetting = this.renderSetting.bind(this) 13 | this.changeSetting = this.changeSetting.bind(this) 14 | } 15 | 16 | changeSetting(plugin, label, value) { 17 | const values = { 18 | ...this.state.values, 19 | [label]: value, 20 | } 21 | 22 | this.setState({ values }) 23 | config.set('plugins', { 24 | ...config.get('plugins'), 25 | [this.props.name]: values, 26 | }) 27 | } 28 | 29 | renderSetting(key) { 30 | const setting = this.props.settings[key] 31 | const { defaultValue, label, ...restProps } = setting 32 | const value = key in this.state.values ? this.state.values[key] : defaultValue 33 | 34 | return ( 35 | this.changeSetting(this.props.name, key, newValue)} 40 | {...restProps} 41 | /> 42 | ) 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | { 49 | Object.keys(this.props.settings).map(this.renderSetting) 50 | } 51 |
52 | ) 53 | } 54 | } 55 | 56 | Settings.propTypes = { 57 | name: PropTypes.string.isRequired, 58 | settings: PropTypes.object.isRequired, 59 | } 60 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0 */ 2 | const webpack = require('webpack'); 3 | const baseConfig = require('./webpack.config.base'); 4 | 5 | const config = { 6 | ...baseConfig, 7 | 8 | devtool: 'inline-source-map', 9 | 10 | entry: { 11 | background: [ 12 | 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', 13 | './app/background/background', 14 | ], 15 | main: [ 16 | 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', 17 | './app/main/main', 18 | ] 19 | }, 20 | 21 | output: { 22 | ...baseConfig.output, 23 | publicPath: 'http://localhost:3000/dist/' 24 | }, 25 | 26 | module: { 27 | ...baseConfig.module, 28 | rules: [ 29 | ...baseConfig.module.rules, 30 | 31 | { 32 | test: /global\.css$/, 33 | use: [ 34 | 'style-loader', 35 | 'css-loader?sourceMap' 36 | ] 37 | }, 38 | 39 | { 40 | test: /^((?!global).)*\.css$/, 41 | use: [ 42 | 'style-loader', 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | modules: true, 47 | sourceMap: true, 48 | importLoaders: 1, 49 | localIdentName: '[name]__[local]___[hash:base64:5]' 50 | } 51 | }, 52 | 'postcss-loader' 53 | ] 54 | } 55 | ] 56 | }, 57 | 58 | plugins: [ 59 | ...baseConfig.plugins, 60 | new webpack.LoaderOptionsPlugin({ 61 | debug: true 62 | }), 63 | new webpack.HotModuleReplacementPlugin(), 64 | ], 65 | 66 | target: 'electron-renderer' 67 | }; 68 | 69 | module.exports = config; 70 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | const baseConfig = require('./webpack.config.base') 5 | const OptimizeJsPlugin = require('optimize-js-plugin') 6 | const Visualizer = require('webpack-visualizer-plugin') 7 | 8 | const config = { 9 | ...baseConfig, 10 | 11 | devtool: 'source-map', 12 | 13 | entry: { 14 | main: './app/main/main', 15 | background: './app/background/background' 16 | }, 17 | 18 | output: { 19 | ...baseConfig.output, 20 | path: path.join(__dirname, 'app/dist'), 21 | publicPath: '../dist/' 22 | }, 23 | 24 | module: { 25 | ...baseConfig.module, 26 | 27 | rules: [ 28 | ...baseConfig.module.rules, 29 | 30 | { 31 | test: /global\.css$/, 32 | use: ExtractTextPlugin.extract({ 33 | fallback: 'style-loader', 34 | use: 'css-loader' 35 | }) 36 | }, 37 | 38 | { 39 | test: /^((?!global).)*\.css$/, 40 | use: ExtractTextPlugin.extract({ 41 | fallback: 'style-loader', 42 | use: [ 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | modules: true, 47 | importLoaders: 1, 48 | localIdentName: '[name]__[local]___[hash:base64:5]' 49 | } 50 | }, 51 | 'postcss-loader' 52 | ] 53 | }) 54 | } 55 | ] 56 | }, 57 | plugins: [ 58 | ...baseConfig.plugins, 59 | new ExtractTextPlugin({ 60 | filename: '[name].css', 61 | allChunks: true, 62 | ignoreOrder: true 63 | }), 64 | new OptimizeJsPlugin({ 65 | sourceMap: false 66 | }) 67 | ], 68 | 69 | target: 'electron-renderer' 70 | } 71 | 72 | if (process.env.ANALYZE) { 73 | config.plugins.push(new Visualizer()) 74 | } 75 | 76 | module.exports = config 77 | -------------------------------------------------------------------------------- /app/lib/getWindowPosition.js: -------------------------------------------------------------------------------- 1 | import { screen } from 'electron' 2 | import config from './config' 3 | 4 | import { 5 | WINDOW_WIDTH, 6 | INPUT_HEIGHT, 7 | RESULT_HEIGHT, 8 | MIN_VISIBLE_RESULTS, 9 | } from '../main/constants/ui' 10 | 11 | 12 | /** 13 | * Returns true if a window is at least partially visible on the display 14 | */ 15 | const isVisible = (windowBounds, displayBounds) => 16 | !(windowBounds.x > displayBounds.x + displayBounds.width || 17 | windowBounds.x + windowBounds.width < displayBounds.x || 18 | windowBounds.y > displayBounds.y + displayBounds.height || 19 | windowBounds.y + windowBounds.height < displayBounds.y) 20 | 21 | 22 | /** 23 | * Computes window position 24 | */ 25 | export default ({ width, heightWithResults }) => { 26 | const winWidth = typeof width !== 'undefined' ? width : WINDOW_WIDTH 27 | const winHeight = typeof heightWithResults !== 'undefined' 28 | ? heightWithResults 29 | : MIN_VISIBLE_RESULTS * RESULT_HEIGHT + INPUT_HEIGHT 30 | 31 | const display = screen.getPrimaryDisplay() 32 | const positions = config.get('positions') || {} 33 | 34 | if (display.id in positions) { 35 | const [x, y] = positions[display.id] 36 | const windowBounds = { x, y, winWidth, winHeight } 37 | const isWindowVisible = (disp) => isVisible(windowBounds, disp.bounds) 38 | 39 | if (isWindowVisible(display)) { 40 | return [x, y] 41 | } 42 | 43 | // The window was moved from the primary screen to a different one. 44 | // We have to check that the window will be visible somewhere among the attached displays. 45 | const displays = screen.getAllDisplays() 46 | const isVisibleSomewhere = displays.some(isWindowVisible) 47 | 48 | if (isVisibleSomewhere) { 49 | return [x, y] 50 | } 51 | } 52 | 53 | const x = parseInt(display.bounds.x + (display.workAreaSize.width - winWidth) / 2, 10) 54 | const y = parseInt(display.bounds.y + (display.workAreaSize.height - winHeight) / 2, 10) 55 | return [x, y] 56 | } 57 | 58 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/initializeAsync.js: -------------------------------------------------------------------------------- 1 | import loadPlugins from './loadPlugins' 2 | import getInstalledPlugins from './getInstalledPlugins' 3 | import { client } from 'lib/plugins' 4 | import config from 'lib/config' 5 | import { flow, filter, map, property } from 'lodash/fp' 6 | 7 | const DEFAULT_PLUGINS = [ 8 | process.platform === 'darwin' ? 'cerebro-mac-apps' : 'cerebro-basic-apps', 9 | 'cerebro-google', 10 | 'cerebro-math', 11 | 'cerebro-converter', 12 | 'cerebro-open-web', 13 | 'cerebro-files-nav' 14 | ] 15 | 16 | /** 17 | * Check plugins for updates and start plugins autoupdater 18 | */ 19 | function checkForUpdates() { 20 | console.log('Run plugins autoupdate') 21 | loadPlugins().then(flow( 22 | filter(property('isUpdateAvailable')), 23 | map(plugin => client.update(plugin.name)) 24 | )).then(promises => Promise.all(promises).then(() => promises.length)) 25 | .then(updatedPlugins => { 26 | console.log( 27 | updatedPlugins > 0 28 | ? `${updatedPlugins} plugins are updated` 29 | : 'All plugins are up to date' 30 | ) 31 | }) 32 | 33 | // Run autoupdate every 12 hours 34 | setTimeout(checkForUpdates, 12 * 60 * 60 * 1000) 35 | } 36 | 37 | /** 38 | * Migrate plugins: default plugins were extracted to separate packages 39 | * so if default plugins are not installed – start installation 40 | */ 41 | function migratePlugins() { 42 | if (config.get('isMigratedPlugins')) { 43 | // Plugins are already migrated 44 | return 45 | } 46 | 47 | console.log('Start installation for default plugins') 48 | 49 | getInstalledPlugins().then(installedPlugins => { 50 | const promises = flow( 51 | filter(plugin => !installedPlugins[plugin]), 52 | map(plugin => client.install(plugin)) 53 | )(DEFAULT_PLUGINS) 54 | 55 | Promise.all(promises).then(() => { 56 | console.log('All default plugins are installed!') 57 | config.set('isMigratedPlugins', true) 58 | }) 59 | }) 60 | } 61 | 62 | export default () => { 63 | checkForUpdates() 64 | migratePlugins() 65 | } 66 | -------------------------------------------------------------------------------- /test/actions/search.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import searchInjector from 'inject-loader!../../app/main/actions/search' 3 | 4 | import { 5 | MOVE_CURSOR, 6 | SELECT_ELEMENT, 7 | UPDATE_RESULT, 8 | HIDE_RESULT, 9 | RESET, 10 | } from '../../app/main/constants/actionTypes' 11 | 12 | const testPlugin = { 13 | fn: () => {} 14 | } 15 | 16 | const pluginsMock = { 17 | 'test-plugin': testPlugin 18 | } 19 | 20 | 21 | const actions = searchInjector({ 22 | electron: {}, 23 | '../plugins/': pluginsMock, 24 | 'lib/config': {}, 25 | 'lib/plugins': { 26 | get: () => undefined 27 | } 28 | }) 29 | 30 | describe('reset', () => { 31 | it('returns valid action', () => { 32 | expect(actions.reset()).toEqual({ 33 | type: RESET, 34 | }) 35 | }) 36 | }) 37 | 38 | describe('moveCursor', () => { 39 | it('returns valid action for +1', () => { 40 | expect(actions.moveCursor(1)).toEqual({ 41 | type: MOVE_CURSOR, 42 | payload: 1 43 | }) 44 | }) 45 | 46 | it('returns valid action for -1', () => { 47 | expect(actions.moveCursor(-1)).toEqual({ 48 | type: MOVE_CURSOR, 49 | payload: -1 50 | }) 51 | }) 52 | }) 53 | 54 | describe('selectElement', () => { 55 | it('returns valid action', () => { 56 | expect(actions.selectElement(15)).toEqual({ 57 | type: SELECT_ELEMENT, 58 | payload: 15 59 | }) 60 | }) 61 | }) 62 | 63 | describe('updateTerm', () => { 64 | context('for empty term', () => { 65 | it('returns reset action', () => { 66 | expect(actions.updateTerm('')).toEqual({ 67 | type: RESET, 68 | }) 69 | }) 70 | }) 71 | }) 72 | 73 | describe('updateElement', () => { 74 | it('returns valid action', () => { 75 | const id = 1 76 | const result = { title: 'updated' } 77 | expect(actions.updateElement(id, result)).toEqual({ 78 | type: UPDATE_RESULT, 79 | payload: { id, result } 80 | }) 81 | }) 82 | }) 83 | 84 | describe('hideElement', () => { 85 | it('returns valid action', () => { 86 | const id = 1 87 | expect(actions.hideElement(id)).toEqual({ 88 | type: HIDE_RESULT, 89 | payload: { id } 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/Preview/styles.css: -------------------------------------------------------------------------------- 1 | .preview { 2 | align-self: flex-start; 3 | width: 100%; 4 | } 5 | 6 | .header { 7 | border-bottom: var(--main-border); 8 | margin-bottom: 15px; 9 | } 10 | 11 | .markdown { 12 | min-width: 100%; 13 | font-size: .8em; 14 | align-self: flex-start; 15 | font-size: 16px; 16 | padding: 10px; 17 | p { 18 | font-size: 1em; 19 | margin: 0 0 10px; 20 | } 21 | h1, h2, h3, h4 { 22 | color: var(--main-font-color); 23 | margin-top: 24px; 24 | margin-bottom: 16px; 25 | font-weight: 500; 26 | line-height: 1.25; 27 | } 28 | h1 { 29 | padding-bottom: 0.3em; 30 | font-size: 2em; 31 | border-bottom: var(--main-border); 32 | } 33 | h2 { 34 | padding-bottom: 0.3em; 35 | font-size: 1.5em; 36 | border-bottom: var(--main-border); 37 | } 38 | pre { 39 | padding: 16px; 40 | overflow: auto; 41 | font-size: 85%; 42 | line-height: 1.45; 43 | filter: invert(10%); 44 | background: var(--main-background-color); 45 | border-radius: 3px; 46 | code { 47 | padding: 0; 48 | background: transparent; 49 | &:before, &:after { 50 | content: none; 51 | } 52 | } 53 | } 54 | blockquote { 55 | border-left: 3px solid #999; 56 | margin: 15px 0; 57 | padding: 5px 15px; 58 | p:last-child { 59 | margin-bottom: 0; 60 | } 61 | } 62 | code { 63 | padding: 0; 64 | padding-top: 0.2em; 65 | padding-bottom: 0.2em; 66 | margin: 0; 67 | font-size: 85%; 68 | background-color: rgba(0,0,0,0.04); 69 | border-radius: 3px; 70 | &:after { 71 | letter-spacing: -0.2em; 72 | content: "\00a0"; 73 | } 74 | &:before { 75 | letter-spacing: -0.2em; 76 | content: "\00a0"; 77 | } 78 | } 79 | img { 80 | max-width: 100%; 81 | } 82 | a { 83 | color: #4078c0; 84 | text-decoration: none; 85 | } 86 | ul { 87 | padding-left: 2em; 88 | list-style-type: disc; 89 | } 90 | li { 91 | list-style-type: disc; 92 | } 93 | li + li { 94 | margin-top: 0.25em; 95 | } 96 | } 97 | 98 | .settingsWrapper { 99 | margin: 15px 0; 100 | } 101 | -------------------------------------------------------------------------------- /app/main.development.js: -------------------------------------------------------------------------------- 1 | import { app, ipcMain, crashReporter } from 'electron' 2 | 3 | import createMainWindow from './main/createWindow' 4 | import createBackgroundWindow from './background/createWindow' 5 | import config from './lib/config' 6 | import AppTray from './main/createWindow/AppTray' 7 | import AppUpdater from './AppUpdater' 8 | 9 | let trayIconSrc = `${__dirname}/tray_icon.png` 10 | if (process.platform === 'darwin') { 11 | trayIconSrc = `${__dirname}/tray_iconTemplate@2x.png` 12 | } else if (process.platform === 'win32') { 13 | trayIconSrc = `${__dirname}/tray_icon.ico` 14 | } 15 | 16 | const isDev = () => ( 17 | process.env.NODE_ENV === 'development' || config.get('developerMode') 18 | ) 19 | 20 | let mainWindow 21 | let backgroundWindow 22 | let tray 23 | let appUpdater 24 | 25 | if (process.env.NODE_ENV !== 'development') { 26 | // Set up crash reporter before creating windows in production builds 27 | crashReporter.start({ 28 | productName: 'Cerebro', 29 | companyName: 'Cerebro', 30 | submitURL: 'http://crashes.cerebroapp.com/post', 31 | autoSubmit: true 32 | }) 33 | } 34 | 35 | app.on('ready', () => { 36 | mainWindow = createMainWindow({ 37 | isDev, 38 | // Main window html 39 | src: `file://${__dirname}/main/index.html`, 40 | }) 41 | 42 | backgroundWindow = createBackgroundWindow({ 43 | src: `file://${__dirname}/background/index.html`, 44 | }) 45 | 46 | tray = new AppTray({ 47 | src: trayIconSrc, 48 | isDev: isDev(), 49 | mainWindow, 50 | backgroundWindow, 51 | }) 52 | 53 | // Show tray icon if it is set in configuration 54 | if (config.get('showInTray')) { 55 | tray.show() 56 | } 57 | 58 | appUpdater = new AppUpdater(mainWindow) 59 | 60 | app.dock && app.dock.hide() 61 | }) 62 | 63 | ipcMain.on('message', (event, payload) => { 64 | const toWindow = event.sender === mainWindow.webContents ? backgroundWindow : mainWindow 65 | toWindow.webContents.send('message', payload) 66 | }) 67 | 68 | ipcMain.on('updateSettings', (event, key, value) => { 69 | mainWindow.settingsChanges.emit(key, value) 70 | 71 | // Show or hide menu bar icon when it is changed in setting 72 | if (key === 'showInTray') { 73 | value ? tray.show() : tray.hide() 74 | } 75 | // Show or hide "development" section in tray menu 76 | if (key === 'developerMode') { 77 | tray.setIsDev(isDev()) 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /app/main/components/ResultsList/styles.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | border-top: var(--main-border); 6 | height: 100%; 7 | position: relative; 8 | } 9 | 10 | .unfocused { 11 | opacity: .5; 12 | } 13 | 14 | .resultsList { 15 | overflow-y: auto; 16 | width: 100%; 17 | min-width: 250px; 18 | } 19 | 20 | .preview { 21 | flex-grow: 2; 22 | padding: 10px 10px 20px 10px; 23 | background-color: var(--main-background-color); 24 | align-items: center; 25 | display: flex; 26 | max-height: 100%; 27 | position: absolute; 28 | left: 250px; 29 | top: 0; 30 | bottom: 0; 31 | right: 0; 32 | overflow: auto; 33 | /* 34 | Instead of using `justify-content: center` we have to use this hack. 35 | In this case child element that is bigger than `.preview ` will be placed on left border 36 | instead of moving outside of container 37 | */ 38 | &::before, &::after { 39 | content: ''; 40 | margin: auto; 41 | } 42 | 43 | &:empty { 44 | display: none; 45 | } 46 | 47 | input { 48 | border: var(--preview-input-border); 49 | background: var(--preview-input-background); 50 | color: var(--preview-input-color); 51 | } 52 | 53 | :global { 54 | /* Styles for react-select */ 55 | .Select { 56 | .Select-control { 57 | border: var(--preview-input-border); 58 | background: var(--preview-input-background); 59 | color: var(--preview-input-color); 60 | } 61 | .Select-menu-outer { 62 | border: var(--preview-input-border); 63 | background: var(--preview-input-background); 64 | } 65 | .Select-input input { 66 | border: 0; 67 | } 68 | .Select-value-label { 69 | color: var(--preview-input-color) !important; 70 | } 71 | .Select-option { 72 | background: var(--preview-input-background); 73 | color: var(--preview-input-color); 74 | &.is-selected { 75 | color: var(--selected-result-title-color); 76 | background: var(--selected-result-background); 77 | } 78 | &.is-focused { 79 | color: var(--selected-result-title-color); 80 | background: var(--selected-result-background); 81 | filter: opacity(50%); 82 | } 83 | } 84 | .Select-option.is-selected { 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/main/createWindow/checkForUpdates.js: -------------------------------------------------------------------------------- 1 | import { dialog, app, shell } from 'electron' 2 | import os from 'os' 3 | import semver from 'semver' 4 | import https from 'https' 5 | 6 | const currentVersion = app.getVersion() 7 | const DEFAULT_DOWNLOAD_URL = 'https://github.com/KELiON/cerebro/releases' 8 | 9 | const TITLE = 'Cerebro Updates' 10 | 11 | const PLATFORM_EXTENSIONS = { 12 | darwin: 'dmg', 13 | linux: 'AppImage', 14 | win32: 'exe' 15 | } 16 | const platform = os.platform() 17 | const installerExtension = PLATFORM_EXTENSIONS[platform] 18 | 19 | const findInstaller = (assets) => { 20 | if (!installerExtension) { 21 | return DEFAULT_DOWNLOAD_URL 22 | } 23 | const regexp = new RegExp(`\.${installerExtension}$`) 24 | const downloadUrl = assets 25 | .map(a => a.browser_download_url) 26 | .find(url => url.match(regexp)) 27 | return downloadUrl || DEFAULT_DOWNLOAD_URL 28 | } 29 | 30 | const getLatestRelease = () => ( 31 | new Promise((resolve, reject) => { 32 | const opts = { 33 | host: 'api.github.com', 34 | path: '/repos/KELiON/cerebro/releases', 35 | headers: { 36 | 'User-Agent': `CerebroApp v${currentVersion}` 37 | } 38 | } 39 | https.get(opts, res => { 40 | let json = '' 41 | res.on('data', (chunk) => { 42 | json += chunk 43 | }) 44 | res.on('end', () => resolve(JSON.parse(json)[0])) 45 | }).on('error', () => reject()) 46 | }) 47 | ) 48 | 49 | export default () => { 50 | getLatestRelease().then(release => { 51 | const version = semver.valid(release.tag_name) 52 | if (version && semver.gt(version, currentVersion)) { 53 | dialog.showMessageBox({ 54 | buttons: ['Skip', 'Download'], 55 | defaultId: 1, 56 | cancelId: 0, 57 | title: TITLE, 58 | message: `New version available: ${version}`, 59 | detail: 'Click download to get it now', 60 | }, (response) => { 61 | if (response === 1) { 62 | const url = findInstaller(release.assets) 63 | shell.openExternal(url) 64 | } 65 | }) 66 | } else { 67 | dialog.showMessageBox({ 68 | title: TITLE, 69 | message: `You are using latest version of Cerebro (${currentVersion})`, 70 | buttons: [] 71 | }) 72 | } 73 | }).catch(err => { 74 | console.log('Catch error!', err) 75 | dialog.showErrorBox( 76 | TITLE, 77 | 'Error fetching latest version', 78 | ) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /app/lib/config.js: -------------------------------------------------------------------------------- 1 | import { app, remote, ipcRenderer } from 'electron' 2 | import fs from 'fs' 3 | import { memoize } from 'cerebro-tools' 4 | import trackEvent from './trackEvent' 5 | import loadThemes from './loadThemes' 6 | 7 | const electronApp = remote ? remote.app : app 8 | 9 | const CONFIG_FILE = `${electronApp.getPath('userData')}/config.json` 10 | 11 | const defaultSettings = memoize(() => { 12 | const locale = electronApp.getLocale() || 'en-US' 13 | const [lang, country] = locale.split('-') 14 | return { 15 | locale, 16 | lang, 17 | country, 18 | // use first theme from loadThemes by default 19 | theme: loadThemes()[0].value, 20 | hotkey: 'Control+Space', 21 | showInTray: true, 22 | firstStart: true, 23 | developerMode: false, 24 | cleanOnHide: true, 25 | skipDonateDialog: false, 26 | lastShownDonateDialog: null, 27 | plugins: {}, 28 | isMigratedPlugins: false 29 | } 30 | }) 31 | 32 | const readConfig = () => { 33 | try { 34 | return JSON.parse(fs.readFileSync(CONFIG_FILE).toString()) 35 | } catch (err) { 36 | return defaultSettings() 37 | } 38 | } 39 | 40 | /** 41 | * Get a value from global configuration 42 | * @param {String} key 43 | * @return {Any} 44 | */ 45 | const get = (key) => { 46 | let config 47 | if (!fs.existsSync(CONFIG_FILE)) { 48 | // Save default config to local storage 49 | config = defaultSettings() 50 | fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)) 51 | } else { 52 | config = readConfig() 53 | } 54 | return config[key] 55 | } 56 | 57 | /** 58 | * Write a value to global config. It immedately rewrites global config 59 | * and notifies all listeners about changes 60 | * 61 | * @param {String} key 62 | * @param {Any} value 63 | */ 64 | const set = (key, value) => { 65 | const config = { 66 | // If after app update some keys were added to config 67 | // we use default values for that keys 68 | ...defaultSettings(), 69 | ...readConfig() 70 | } 71 | config[key] = value 72 | fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)) 73 | // Track settings changes 74 | trackEvent({ 75 | category: 'Settings', 76 | event: `Change ${key}`, 77 | label: value, 78 | }) 79 | if (ipcRenderer) { 80 | console.log('notify main process', key, value) 81 | // Notify main process about settings changes 82 | ipcRenderer.send('updateSettings', key, value) 83 | } 84 | } 85 | 86 | export default { get, set } 87 | -------------------------------------------------------------------------------- /app/main/createWindow/donateDialog.js: -------------------------------------------------------------------------------- 1 | import { dialog, shell } from 'electron' 2 | import config from '../../lib/config' 3 | import trackEvent from '../../lib/trackEvent' 4 | 5 | const now = () => new Date().getTime() 6 | const twoWeeksAgo = () => now() - 1000 * 3600 * 24 * 7 7 | 8 | export const donate = () => { 9 | config.set('skipDonateDialog', true) 10 | shell.openExternal('https://cerebroapp.com/#donate') 11 | } 12 | 13 | export const shouldShow = () => { 14 | if (config.get('firstStart') || config.get('skipDonateDialog')) { 15 | // Do not show donate dialog on first start or when "dont show this message" were chosen 16 | return false 17 | } 18 | const lastShow = config.get('lastShownDonateDialog') 19 | // Show on second start and once per week after first start 20 | return !lastShow || twoWeeksAgo() >= lastShow 21 | } 22 | 23 | /* eslint-disable max-len*/ 24 | const messages = [ 25 | 'Do you like completely free and open-source Cerebro? Support developers with your donation!', 26 | 'Do you like Cerebro? Buy a beer for developers!', 27 | 'Free. Open-source. No ads. It is about Cerebro. Do you like it? Make small donation to support the app', 28 | 'Developers try to make you happy with this free and open-source app. Make them happy too with your donation!' 29 | ] 30 | 31 | const buttons = [ 32 | ['Close', 'Support'], 33 | ['Not now', 'Of course!'], 34 | ['Skip', 'Donate'], 35 | ['Close', 'Make them happy'] 36 | ] 37 | 38 | const skipMessages = [ 39 | "Don't show this message anymore", 40 | "Don't ask me again", 41 | "I won't donate", 42 | "I don't want to see this message again", 43 | ] 44 | /* eslint-enable max-len */ 45 | 46 | export const show = () => { 47 | config.set('lastShownDonateDialog', now()) 48 | const AB = Math.floor(Math.random() * buttons.length) 49 | const track = (event) => { 50 | trackEvent({ 51 | event, 52 | category: 'Donate Dialog', 53 | label: AB 54 | }) 55 | } 56 | 57 | track('show-dialog') 58 | 59 | const options = { 60 | type: 'info', 61 | buttons: buttons[AB], 62 | defaultId: 1, 63 | cancelId: 0, 64 | title: 'Support Cerebro development', 65 | message: messages[AB], 66 | checkboxLabel: skipMessages[AB] 67 | } 68 | 69 | const callback = (id, checkboxChecked) => { 70 | if (checkboxChecked) { 71 | config.set('skipDonateDialog', true) 72 | track('skip-dialog') 73 | } 74 | if (id === 1) { 75 | track('choose-donate') 76 | donate() 77 | } else { 78 | track('cancel') 79 | } 80 | } 81 | 82 | setTimeout(() => { 83 | dialog.showMessageBox(options, callback) 84 | }, 1000) 85 | } 86 | -------------------------------------------------------------------------------- /app/main/plugins/core/settings/Settings/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import Hotkey from './Hotkey' 3 | import countries from './countries' 4 | import { Select, Checkbox, Wrapper } from 'cerebro-ui/Form' 5 | import loadThemes from 'lib/loadThemes' 6 | import styles from './styles.css' 7 | 8 | class Settings extends Component { 9 | constructor(props) { 10 | super(props) 11 | const { get } = this.props 12 | this.state = { 13 | hotkey: get('hotkey'), 14 | showInTray: get('showInTray'), 15 | country: get('country'), 16 | theme: get('theme'), 17 | developerMode: get('developerMode'), 18 | cleanOnHide: get('cleanOnHide'), 19 | pluginsSettings: get('plugins'), 20 | } 21 | this.changeConfig = this.changeConfig.bind(this) 22 | } 23 | changeConfig(key, value) { 24 | this.props.set(key, value) 25 | this.setState({ 26 | [key]: value 27 | }) 28 | } 29 | render() { 30 | const { 31 | hotkey, showInTray, country, theme, developerMode, cleanOnHide 32 | } = this.state 33 | 34 | return ( 35 |
36 | 37 | this.changeConfig('hotkey', key)} 40 | /> 41 | 42 | this.changeConfig('theme', value)} 54 | /> 55 | this.changeConfig('showInTray', value)} 59 | /> 60 | this.changeConfig('developerMode', value)} 64 | /> 65 | this.changeConfig('cleanOnHide', value)} 69 | /> 70 |
71 | ) 72 | } 73 | } 74 | 75 | Settings.propTypes = { 76 | get: PropTypes.func.isRequired, 77 | set: PropTypes.func.isRequired 78 | } 79 | 80 | export default Settings 81 | -------------------------------------------------------------------------------- /app/main/plugins/externalPlugins.js: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce' 2 | import chokidar from 'chokidar' 3 | import path from 'path' 4 | import { modulesDirectory, ensureFiles, settings } from 'lib/plugins' 5 | 6 | const requirePlugin = (pluginPath) => { 7 | try { 8 | let plugin = window.require(pluginPath) 9 | // Fallback for plugins with structure like `{default: {fn: ...}}` 10 | const keys = Object.keys(plugin) 11 | if (keys.length === 1 && keys[0] === 'default') { 12 | plugin = plugin.default 13 | } 14 | return plugin 15 | } catch (error) { 16 | // catch all errors from plugin loading 17 | console.log('Error requiring', pluginPath) 18 | console.log(error) 19 | } 20 | } 21 | 22 | /** 23 | * Validate plugin module signature 24 | * 25 | * @param {Object} plugin 26 | * @return {Boolean} 27 | */ 28 | const isPluginValid = (plugin) => ( 29 | plugin && 30 | // Check existing of main plugin function 31 | typeof plugin.fn === 'function' && 32 | // Check that plugin function accepts 0 or 1 argument 33 | plugin.fn.length <= 1 34 | ) 35 | 36 | ensureFiles() 37 | 38 | const plugins = {} 39 | 40 | const pluginsWatcher = chokidar.watch(modulesDirectory, { depth: 0 }) 41 | 42 | pluginsWatcher.on('unlinkDir', (pluginPath) => { 43 | const name = path.parse(pluginPath).base 44 | const requirePath = window.require.resolve(pluginPath) 45 | console.log(`[${name}] Plugin removed`) 46 | delete window.require.cache[requirePath] 47 | delete plugins[name] 48 | }) 49 | 50 | pluginsWatcher.on('addDir', (pluginPath) => { 51 | const { base, dir } = path.parse(pluginPath) 52 | if (dir !== modulesDirectory) { 53 | return 54 | } 55 | setTimeout(() => { 56 | console.group(`Load plugin: ${base}`) 57 | console.log(`Path: ${pluginPath}...`) 58 | const plugin = requirePlugin(pluginPath) 59 | if (!isPluginValid(plugin)) { 60 | console.log('Plugin is not valid, skipped') 61 | console.groupEnd() 62 | return 63 | } 64 | if (!settings.validate(plugin)) { 65 | console.log('Invalid plugins settings') 66 | console.groupEnd() 67 | return 68 | } 69 | 70 | console.log('Loaded.') 71 | const requirePath = window.require.resolve(pluginPath) 72 | const watcher = chokidar.watch(requirePath, { depth: 0 }) 73 | watcher.on('change', debounce(() => { 74 | console.log(`[${base}] Update plugin`) 75 | delete window.require.cache[requirePath] 76 | plugins[base] = window.require(pluginPath) 77 | console.log(`[${base}] Plugin updated`) 78 | }, 1000)) 79 | console.groupEnd() 80 | plugins[base] = plugin 81 | }, 1000) 82 | }) 83 | 84 | export default plugins 85 | -------------------------------------------------------------------------------- /app/main/css/system-font.css: -------------------------------------------------------------------------------- 1 | /*! system-font.css v1.1.0 | CC0-1.0 License | github.com/jonathantneal/system-font-face */ 2 | 3 | @font-face { 4 | font-family: system; 5 | font-style: normal; 6 | font-weight: 300; 7 | src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma"); 8 | } 9 | 10 | @font-face { 11 | font-family: system; 12 | font-style: italic; 13 | font-weight: 300; 14 | src: local(".SFNSText-LightItalic"), local(".HelveticaNeueDeskInterface-Italic"), local(".LucidaGrandeUI"), local("Ubuntu Light Italic"), local("Segoe UI Light Italic"), local("Roboto-LightItalic"), local("DroidSans"), local("Tahoma"); 15 | } 16 | 17 | @font-face { 18 | font-family: system; 19 | font-style: normal; 20 | font-weight: 400; 21 | src: local(".SFNSText-Regular"), local(".HelveticaNeueDeskInterface-Regular"), local(".LucidaGrandeUI"), local("Ubuntu"), local("Segoe UI"), local("Roboto-Regular"), local("DroidSans"), local("Tahoma"); 22 | } 23 | 24 | @font-face { 25 | font-family: system; 26 | font-style: italic; 27 | font-weight: 400; 28 | src: local(".SFNSText-Italic"), local(".HelveticaNeueDeskInterface-Italic"), local(".LucidaGrandeUI"), local("Ubuntu Italic"), local("Segoe UI Italic"), local("Roboto-Italic"), local("DroidSans"), local("Tahoma"); 29 | } 30 | 31 | @font-face { 32 | font-family: system; 33 | font-style: normal; 34 | font-weight: 500; 35 | src: local(".SFNSText-Medium"), local(".HelveticaNeueDeskInterface-MediumP4"), local(".LucidaGrandeUI"), local("Ubuntu Medium"), local("Segoe UI Semibold"), local("Roboto-Medium"), local("DroidSans-Bold"), local("Tahoma Bold"); 36 | } 37 | 38 | @font-face { 39 | font-family: system; 40 | font-style: italic; 41 | font-weight: 500; 42 | src: local(".SFNSText-MediumItalic"), local(".HelveticaNeueDeskInterface-MediumItalicP4"), local(".LucidaGrandeUI"), local("Ubuntu Medium Italic"), local("Segoe UI Semibold Italic"), local("Roboto-MediumItalic"), local("DroidSans-Bold"), local("Tahoma Bold"); 43 | } 44 | 45 | @font-face { 46 | font-family: system; 47 | font-style: normal; 48 | font-weight: 700; 49 | src: local(".SFNSText-Bold"), local(".HelveticaNeueDeskInterface-Bold"), local(".LucidaGrandeUI"), local("Ubuntu Bold"), local("Roboto-Bold"), local("DroidSans-Bold"), local("Segoe UI Bold"), local("Tahoma Bold"); 50 | } 51 | 52 | @font-face { 53 | font-family: system; 54 | font-style: italic; 55 | font-weight: 700; 56 | src: local(".SFNSText-BoldItalic"), local(".HelveticaNeueDeskInterface-BoldItalic"), local(".LucidaGrandeUI"), local("Ubuntu Bold Italic"), local("Roboto-BoldItalic"), local("DroidSans-Bold"), local("Segoe UI Bold Italic"), local("Tahoma Bold"); 57 | } 58 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Preview from './Preview' 3 | import { search } from 'cerebro-tools' 4 | import { shell } from 'electron' 5 | import loadPlugins from './loadPlugins' 6 | import icon from '../icon.png' 7 | import * as format from './format' 8 | import { flow, map, partialRight, tap } from 'lodash/fp' 9 | import { partition } from 'lodash' 10 | import initializeAsync from './initializeAsync' 11 | 12 | const toString = ({ name, description }) => [name, description].join(' ') 13 | const categories = [ 14 | ['Updates', plugin => plugin.isUpdateAvailable], 15 | ['Installed', plugin => plugin.isInstalled], 16 | ['Available', plugin => plugin.name], 17 | ] 18 | 19 | const updatePlugin = (update, name) => { 20 | loadPlugins().then(plugins => { 21 | const updatedPlugin = plugins.find(plugin => plugin.name === name) 22 | update(name, { 23 | title: `${format.name(updatedPlugin.name)} (${format.version(updatedPlugin)})`, 24 | getPreview: () => ( 25 | updatePlugin(update, name)} 29 | /> 30 | ) 31 | }) 32 | }) 33 | } 34 | 35 | const pluginToResult = update => plugin => { 36 | if (typeof plugin === 'string') { 37 | return { title: plugin } 38 | } 39 | 40 | return { 41 | icon, 42 | id: plugin.name, 43 | title: `${format.name(plugin.name)} (${format.version(plugin)})`, 44 | subtitle: format.description(plugin.description), 45 | onSelect: () => shell.openExternal(plugin.repo), 46 | getPreview: () => ( 47 | updatePlugin(update, plugin.name)} 51 | /> 52 | ) 53 | } 54 | } 55 | 56 | const categorize = (plugins, callback) => { 57 | const result = [] 58 | let remainder = plugins 59 | 60 | categories.forEach(category => { 61 | const [title, filter] = category 62 | const [matched, others] = partition(remainder, filter) 63 | if (matched.length) result.push(title, ...matched) 64 | remainder = others 65 | }) 66 | 67 | plugins.splice(0, plugins.length) 68 | plugins.push(...result) 69 | callback() 70 | } 71 | 72 | const fn = ({ term, display, hide, update }) => { 73 | const match = term.match(/^plugins?\s*(.+)?$/i) 74 | if (match) { 75 | display({ 76 | icon, 77 | id: 'loading', 78 | title: 'Looking for plugins...' 79 | }) 80 | loadPlugins().then(flow( 81 | partialRight(search, [match[1], toString]), 82 | tap(plugins => categorize(plugins, () => hide('loading'))), 83 | map(pluginToResult(update)), 84 | display 85 | )) 86 | } 87 | } 88 | 89 | export default { 90 | icon, 91 | fn, 92 | initializeAsync, 93 | name: 'Manage plugins', 94 | keyword: 'plugins' 95 | } 96 | -------------------------------------------------------------------------------- /app/main/createWindow/AppTray.js: -------------------------------------------------------------------------------- 1 | import { Menu, Tray, app } from 'electron' 2 | import showWindowWithTerm from './showWindowWithTerm' 3 | import toggleWindow from './toggleWindow' 4 | import checkForUpdates from './checkForUpdates' 5 | import { donate } from './donateDialog' 6 | 7 | /** 8 | * Class that controls state of icon in menu bar 9 | */ 10 | export default class AppTray { 11 | /** 12 | * @param {String} options.src Absolute path for tray icon 13 | * @param {Function} options.isDev Development mode or not 14 | * @param {BrowserWindow} options.mainWindow 15 | * @param {BrowserWindow} options.backgroundWindow 16 | * @return {AppTray} 17 | */ 18 | constructor(options) { 19 | this.tray = null 20 | this.options = options 21 | } 22 | /** 23 | * Show application icon in menu bar 24 | */ 25 | show() { 26 | const tray = new Tray(this.options.src) 27 | tray.setToolTip('Cerebro') 28 | tray.setContextMenu(this.buildMenu()) 29 | this.tray = tray 30 | } 31 | setIsDev(isDev) { 32 | this.options.isDev = isDev 33 | if (this.tray) { 34 | this.tray.setContextMenu(this.buildMenu()) 35 | } 36 | } 37 | buildMenu() { 38 | const { mainWindow, backgroundWindow, isDev } = this.options 39 | const separator = { type: 'separator' } 40 | 41 | const template = [ 42 | { 43 | label: 'Toggle Cerebro', 44 | click: () => toggleWindow(mainWindow) 45 | }, 46 | separator, 47 | { 48 | label: 'Plugins', 49 | click: () => showWindowWithTerm(mainWindow, 'plugins'), 50 | }, 51 | { 52 | label: 'Preferences...', 53 | click: () => showWindowWithTerm(mainWindow, 'settings'), 54 | }, 55 | separator, 56 | { 57 | label: 'Check for updates', 58 | click: () => checkForUpdates(), 59 | }, 60 | separator, 61 | { 62 | label: 'Donate...', 63 | click: donate 64 | } 65 | ] 66 | 67 | if (isDev) { 68 | template.push(separator) 69 | template.push({ 70 | label: 'Development', 71 | submenu: [{ 72 | label: 'DevTools (main)', 73 | click: () => mainWindow.webContents.openDevTools({ mode: 'detach' }) 74 | }, { 75 | label: 'DevTools (background)', 76 | click: () => backgroundWindow.webContents.openDevTools({ mode: 'detach' }) 77 | }, { 78 | label: 'Reload', 79 | click: () => { 80 | mainWindow.reload() 81 | backgroundWindow.reload() 82 | backgroundWindow.hide() 83 | } 84 | }] 85 | }) 86 | } 87 | 88 | template.push(separator) 89 | template.push({ 90 | label: 'Quit Cerebro', 91 | click: () => app.quit() 92 | }) 93 | return Menu.buildFromTemplate(template) 94 | } 95 | /** 96 | * Hide icon in menu bar 97 | */ 98 | hide() { 99 | if (this.tray) { 100 | this.tray.destroy() 101 | this.tray = null 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/main/createWindow/buildMenu.js: -------------------------------------------------------------------------------- 1 | import { Menu, shell, app } from 'electron' 2 | 3 | export default (mainWindow) => { 4 | const template = [{ 5 | label: 'Electron', 6 | submenu: [{ 7 | label: 'About ElectronReact', 8 | selector: 'orderFrontStandardAboutPanel:' 9 | }, { 10 | type: 'separator' 11 | }, { 12 | label: 'Services', 13 | submenu: [] 14 | }, { 15 | type: 'separator' 16 | }, { 17 | label: 'Hide ElectronReact', 18 | accelerator: 'Command+H', 19 | selector: 'hide:' 20 | }, { 21 | label: 'Hide Others', 22 | accelerator: 'Command+Shift+H', 23 | selector: 'hideOtherApplications:' 24 | }, { 25 | label: 'Show All', 26 | selector: 'unhideAllApplications:' 27 | }, { 28 | type: 'separator' 29 | }, { 30 | label: 'Quit', 31 | accelerator: 'Command+Q', 32 | click() { 33 | app.quit() 34 | } 35 | }] 36 | }, { 37 | label: 'Edit', 38 | submenu: [{ 39 | label: 'Undo', 40 | accelerator: 'Command+Z', 41 | selector: 'undo:' 42 | }, { 43 | label: 'Redo', 44 | accelerator: 'Shift+Command+Z', 45 | selector: 'redo:' 46 | }, { 47 | type: 'separator' 48 | }, { 49 | label: 'Cut', 50 | accelerator: 'Command+X', 51 | selector: 'cut:' 52 | }, { 53 | label: 'Copy', 54 | accelerator: 'Command+C', 55 | selector: 'copy:' 56 | }, { 57 | label: 'Paste', 58 | accelerator: 'Command+V', 59 | selector: 'paste:' 60 | }, { 61 | label: 'Select All', 62 | accelerator: 'Command+A', 63 | selector: 'selectAll:' 64 | }] 65 | }, { 66 | label: 'View', 67 | submenu: [{ 68 | label: 'Toggle Full Screen', 69 | accelerator: 'Ctrl+Command+F', 70 | click() { 71 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 72 | } 73 | }] 74 | }, { 75 | label: 'Window', 76 | submenu: [{ 77 | label: 'Minimize', 78 | accelerator: 'Command+M', 79 | selector: 'performMiniaturize:' 80 | }, { 81 | label: 'Close', 82 | accelerator: 'Command+W', 83 | selector: 'performClose:' 84 | }, { 85 | type: 'separator' 86 | }, { 87 | label: 'Bring All to Front', 88 | selector: 'arrangeInFront:' 89 | }] 90 | }, { 91 | label: 'Help', 92 | submenu: [{ 93 | label: 'Learn More', 94 | click() { 95 | shell.openExternal('http://electron.atom.io') 96 | } 97 | }, { 98 | label: 'Documentation', 99 | click() { 100 | shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme') 101 | } 102 | }, { 103 | label: 'Community Discussions', 104 | click() { 105 | shell.openExternal('https://discuss.atom.io/c/electron') 106 | } 107 | }, { 108 | label: 'Search Issues', 109 | click() { 110 | shell.openExternal('https://github.com/atom/electron/issues') 111 | } 112 | }] 113 | }] 114 | 115 | const menu = Menu.buildFromTemplate(template) 116 | Menu.setApplicationMenu(menu) 117 | } 118 | -------------------------------------------------------------------------------- /app/main/components/ResultsList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import Row from './Row' 3 | import styles from './styles.css' 4 | import { VirtualScroll } from 'react-virtualized' 5 | import { bind } from 'lodash-decorators' 6 | 7 | import { RESULT_HEIGHT } from '../../constants/ui' 8 | 9 | class ResultsList extends Component { 10 | @bind() 11 | rowRenderer({ index }) { 12 | const result = this.props.results[index] 13 | const attrs = { 14 | ...result, 15 | // TODO: think about events 16 | // In some cases action should be executed and window should be closed 17 | // In some cases we should autocomplete value 18 | selected: index === this.props.selected, 19 | onSelect: (event) => this.props.onSelect(result, event), 20 | // Move selection to item under cursor 21 | onMouseMove: (event) => { 22 | const { selected, mainInputFocused, onItemHover } = this.props 23 | const { movementX, movementY } = event.nativeEvent 24 | if (index === selected || !mainInputFocused) { 25 | return false 26 | } 27 | if (movementX || movementY) { 28 | // Hover item only when we had real movement of mouse 29 | // We should prevent changing of selection when user uses keyboard 30 | onItemHover(index) 31 | } 32 | }, 33 | key: result.id, 34 | } 35 | return 36 | } 37 | renderPreview() { 38 | const selected = this.props.results[this.props.selected] 39 | if (!selected.getPreview) { 40 | return null 41 | } 42 | const preview = selected.getPreview() 43 | if (typeof preview === 'string') { 44 | // Fallback for html previews intead of react component 45 | return
46 | } 47 | return preview 48 | } 49 | render() { 50 | const { results, selected, visibleResults, mainInputFocused } = this.props 51 | const classNames = [ 52 | styles.resultsList, 53 | mainInputFocused ? styles.focused : styles.unfocused 54 | ].join(' ') 55 | if (results.length === 0) { 56 | return null 57 | } 58 | return ( 59 |
60 | result.title)} 72 | // Disable accesebility of VirtualScroll by tab 73 | tabIndex={null} 74 | /> 75 |
76 | {this.renderPreview()} 77 |
78 |
79 | ) 80 | } 81 | } 82 | 83 | ResultsList.propTypes = { 84 | results: PropTypes.array, 85 | selected: PropTypes.number, 86 | visibleResults: PropTypes.number, 87 | onItemHover: PropTypes.func, 88 | onSelect: PropTypes.func, 89 | mainInputFocused: PropTypes.bool, 90 | } 91 | 92 | export default ResultsList 93 | -------------------------------------------------------------------------------- /app/main/plugins/core/settings/Settings/Hotkey.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import styles from './styles.css' 3 | 4 | const ASCII = { 5 | 188: '44', 6 | 109: '45', 7 | 190: '46', 8 | 191: '47', 9 | 192: '96', 10 | 220: '92', 11 | 222: '39', 12 | 221: '93', 13 | 219: '91', 14 | 173: '45', 15 | 187: '61', 16 | 186: '59', 17 | 189: '45' 18 | } 19 | 20 | const SHIFT_UPS = { 21 | 96: '~', 22 | 49: '!', 23 | 50: '@', 24 | 51: '#', 25 | 52: '$', 26 | 53: '%', 27 | 54: '^', 28 | 55: '&', 29 | 56: '*', 30 | 57: '(', 31 | 48: ')', 32 | 45: '_', 33 | 61: '+', 34 | 91: '{', 35 | 93: '}', 36 | 92: '|', 37 | 59: ':', 38 | 39: '"', 39 | 44: '<', 40 | 46: '>', 41 | 47: '?' 42 | } 43 | 44 | const KEYCODES = { 45 | 8: 'Backspace', 46 | 9: 'Tab', 47 | 13: 'Enter', 48 | 27: 'Esc', 49 | 32: 'Space', 50 | 37: 'Left', 51 | 38: 'Up', 52 | 39: 'Right', 53 | 40: 'Down', 54 | 112: 'F1', 55 | 113: 'F2', 56 | 114: 'F3', 57 | 115: 'F4', 58 | 116: 'F5', 59 | 117: 'F6', 60 | 118: 'F7', 61 | 119: 'F8', 62 | 120: 'F9', 63 | 121: 'F10', 64 | 122: 'F11', 65 | 123: 'F12', 66 | } 67 | 68 | const osKeyDelimiter = process.platform === 'darwin' ? '' : '+' 69 | 70 | const keyToSign = (key) => { 71 | if (process.platform === 'darwin') { 72 | return key.replace(/control/i, '⌃') 73 | .replace(/alt/i, '⌥') 74 | .replace(/shift/i, '⇧') 75 | .replace(/command/i, '⌘') 76 | .replace(/enter/i, '↩') 77 | .replace(/backspace/i, '⌫') 78 | } 79 | return key 80 | } 81 | 82 | const charCodeToSign = ({ keyCode, shiftKey }) => { 83 | const code = ASCII[keyCode] ? ASCII[keyCode] : keyCode 84 | if (!shiftKey && (code >= 65 && code <= 90)) { 85 | return String.fromCharCode(code + 32) 86 | } 87 | if (shiftKey && SHIFT_UPS[code]) { 88 | return SHIFT_UPS[code] 89 | } 90 | if (KEYCODES[code]) { 91 | return KEYCODES[code] 92 | } 93 | const valid = 94 | (code > 47 && code < 58) || // number keys 95 | (code > 64 && code < 91) || // letter keys 96 | (code > 95 && code < 112) || // numpad keys 97 | (code > 185 && code < 193) || // ;=,-./` (in order) 98 | (code > 218 && code < 223) // [\]' (in order) 99 | return valid ? String.fromCharCode(code) : null 100 | } 101 | 102 | class Hotkey extends Component { 103 | onKeyDown(event) { 104 | if (!event.ctrlKey && !event.altKey && !event.metaKey) { 105 | // Do not allow to set global shorcut without modifier keys 106 | // At least one of alt, cmd or ctrl is required 107 | return 108 | } 109 | event.preventDefault() 110 | event.stopPropagation() 111 | const key = charCodeToSign(event) 112 | if (!key) { 113 | return 114 | } 115 | const keys = [] 116 | if (event.ctrlKey) keys.push('Control') 117 | if (event.altKey) keys.push('Alt') 118 | if (event.shiftKey) keys.push('Shift') 119 | if (event.metaKey) keys.push('Command') 120 | keys.push(key) 121 | this.props.onChange(keys.join('+')) 122 | } 123 | render() { 124 | const { hotkey } = this.props 125 | const keys = hotkey.split('+').map(keyToSign).join(osKeyDelimiter) 126 | return ( 127 |
128 | 134 |
135 | ) 136 | } 137 | } 138 | 139 | Hotkey.propTypes = { 140 | hotkey: PropTypes.string.isRequired, 141 | onChange: PropTypes.func.isRequired, 142 | } 143 | 144 | export default Hotkey 145 | -------------------------------------------------------------------------------- /docs/plugins/examples.md: -------------------------------------------------------------------------------- 1 | # Plugins → Examples 2 | You always can check out source code of existing plugins, like: 3 | 4 | * [cerebro-emoj](https://github.com/KELiON/cerebro-emoj) 5 | * [cerebro-gif](https://github.com/KELiON/cerebro-gif) 6 | * [cerebro-kill](https://github.com/KELiON/cerebro-kill) 7 | * [cerebro-ip](https://github.com/KELiON/cerebro-ip) 8 | 9 | 10 | Or any of [core](../../app/main/plugins/core) plugins. 11 | 12 | ### Using `id` 13 | ```js 14 | const plugin = ({display}) => { 15 | display({ 16 | id: 'my-id', 17 | title: 'Loading' 18 | }) 19 | fetchResult().then((result) => { 20 | display({ 21 | id: 'my-id', 22 | title: `Fetched result: ${result}` 23 | }) 24 | }); 25 | } 26 | 27 | module.exports = { 28 | fn: plugin, 29 | } 30 | ``` 31 | 32 | ### Using `icon` 33 | ```js 34 | const icon = require('[path-to-icon]/icon.png'); 35 | 36 | const plugin = ({display}) => { 37 | display({ 38 | icon, 39 | title: 'Title', 40 | subtitle: 'Subtitle' 41 | }); 42 | } 43 | 44 | module.exports = { 45 | fn: plugin, 46 | } 47 | ``` 48 | 49 | ### Using `keyword` and `name` 50 | ```js 51 | const plugin = (scope) => { 52 | const match = scope.term.match(/^emoj\s(.+)/); 53 | if (match) { 54 | searchEmojis(match[1]).then(results => { 55 | scope.display(results) 56 | }) 57 | }; 58 | } 59 | 60 | module.exports = { 61 | name: 'Search emojis...' 62 | fn: plugin, 63 | keyword: 'emoj' 64 | } 65 | 66 | ``` 67 | 68 | ### Using `initialize` 69 | ```js 70 | // Some data needed for your plugin 71 | let data; 72 | 73 | // Fetch some data only on app initialization 74 | const initialize = () => { 75 | fetchYourData().then(result => { 76 | data = result 77 | }); 78 | } 79 | 80 | const plugin = (scope) => { 81 | const results = search(data, scope.term); 82 | scope.display(results); 83 | } 84 | 85 | module.exports = { 86 | initialize: initialize, 87 | fn: plugin 88 | } 89 | ``` 90 | 91 | 92 | ### Using `initializeAsync` and `onMessage` 93 | ```js 94 | let data; 95 | 96 | // Run some long-running initialization process in background 97 | const initialize = (cb) => { 98 | fetchYourData().then(cb); 99 | // and re-fetch this information once in 1h 100 | setInterval(() => { 101 | initialize(cb); 102 | }, 60 * 3600); 103 | } 104 | 105 | const onMessage = (results) => { 106 | data = results; 107 | } 108 | 109 | const plugin = (scope) => { 110 | const results = search(data, scope.term); 111 | scope.display(results); 112 | } 113 | 114 | module.exports = { 115 | initializeAsync: initialize, 116 | onMessage: onMessage, 117 | fn: plugin 118 | } 119 | ``` 120 | 121 | ### Using `cerebro-tools` 122 | ```js 123 | const { memoize, search } = require('cerebro-tools'); 124 | const preprocessJson = require('./preprocessJson'); 125 | 126 | // Memoize your fetched data from external API 127 | const fetchData = memoize(() => { 128 | return fetch('http://api/url') 129 | .then(response => response.json()) 130 | .then(preprocessJson) 131 | }); 132 | 133 | const plugin = ({term, display}) => { 134 | fetchData().then(data => { 135 | const results = search(data, term, (el) => el.name); 136 | display(term); 137 | }) 138 | } 139 | 140 | module.exports = { 141 | fn: plugin 142 | }; 143 | ``` 144 | 145 | ### using `settings` 146 | ```js 147 | const plugin = ({ display, settings }) => { 148 | const icon = require('[path-to-icon]/icon.png'); 149 | 150 | display({ 151 | icon: settings.icon ? icon : '', 152 | title: `${settings.username}, you have been around for ${settings.age}`, 153 | subtitle: `Favorite languages: ${settings.languages.join(',')}`, 154 | }) 155 | } 156 | 157 | module.exports = { 158 | fn: plugin, 159 | settings: { 160 | username: { type: 'string' }, 161 | age: { type: 'number', defaultValue: 42 }, 162 | icon: { type: 'bool' }, 163 | languages: { 164 | type: 'option', 165 | description: 'Your favorite programming languages' 166 | options: ['JavaScript', 'Haskell', 'Rust'], 167 | multi: true, 168 | createable: true, 169 | } 170 | } 171 | } 172 | 173 | ``` 174 | -------------------------------------------------------------------------------- /app/main/reducers/search.js: -------------------------------------------------------------------------------- 1 | /* eslint no-shadow: [2, { "allow": ["comments"] }] */ 2 | 3 | import { 4 | UPDATE_TERM, 5 | MOVE_CURSOR, 6 | SELECT_ELEMENT, 7 | SHOW_RESULT, 8 | HIDE_RESULT, 9 | UPDATE_RESULT, 10 | RESET, 11 | CHANGE_VISIBLE_RESULTS 12 | } from '../constants/actionTypes' 13 | 14 | import { MIN_VISIBLE_RESULTS } from '../constants/ui' 15 | 16 | import uniq from 'lodash/uniq' 17 | import orderBy from 'lodash/orderBy' 18 | 19 | const initialState = { 20 | // Search term in main input 21 | term: '', 22 | // Store last used term in separate field 23 | prevTerm: '', 24 | // Array of ids of results 25 | resultIds: [], 26 | resultsById: {}, 27 | // Index of selected result 28 | selected: 0, 29 | // Count of visible results 30 | visibleResults: MIN_VISIBLE_RESULTS 31 | } 32 | 33 | 34 | /** 35 | * Normalize index of selected item. 36 | * Index should be >= 0 and <= results.length 37 | * 38 | * @param {Integer} index 39 | * @param {Integer} length current count of found results 40 | * @return {Integer} normalized index 41 | */ 42 | function normalizeSelection(index, length) { 43 | const normalizedIndex = index % length 44 | return normalizedIndex < 0 ? length + normalizedIndex : normalizedIndex 45 | } 46 | 47 | // Function that does nothing 48 | const noon = () => {} 49 | 50 | function normalizeResult(result) { 51 | return { 52 | ...result, 53 | onFocus: result.onFocus || noon, 54 | onBlur: result.onFocus || noon, 55 | onSelect: result.onSelect || noon, 56 | } 57 | } 58 | 59 | export default function search(state = initialState, { type, payload }) { 60 | switch (type) { 61 | case UPDATE_TERM: { 62 | return { 63 | ...state, 64 | term: payload, 65 | resultIds: [], 66 | selected: 0 67 | } 68 | } 69 | case MOVE_CURSOR: { 70 | let selected = state.selected 71 | const resultIds = state.resultIds 72 | selected += payload 73 | selected = normalizeSelection(selected, resultIds.length) 74 | return { 75 | ...state, 76 | selected, 77 | } 78 | } 79 | case SELECT_ELEMENT: { 80 | const selected = normalizeSelection(payload, state.resultIds.length) 81 | return { 82 | ...state, 83 | selected, 84 | } 85 | } 86 | case UPDATE_RESULT: { 87 | const { id, result } = payload 88 | const { resultsById } = state 89 | const newResult = { 90 | ...resultsById[id], 91 | ...result 92 | } 93 | return { 94 | ...state, 95 | resultsById: { 96 | ...resultsById, 97 | [id]: newResult 98 | } 99 | } 100 | } 101 | case HIDE_RESULT: { 102 | const { id } = payload 103 | let { resultsById, resultIds } = state 104 | resultIds = resultIds.filter(resultId => resultId !== id) 105 | 106 | resultsById = resultIds.reduce((acc, resultId) => ({ 107 | ...acc, 108 | [resultId]: resultsById[resultId] 109 | }), {}) 110 | 111 | return { 112 | ...state, 113 | resultsById, 114 | resultIds 115 | } 116 | } 117 | case SHOW_RESULT: { 118 | const { term, result } = payload 119 | if (term !== state.term) { 120 | // Do not show this result if term was changed 121 | return state 122 | } 123 | let { resultsById, resultIds } = state 124 | 125 | result.forEach(res => { 126 | resultsById = { 127 | ...resultsById, 128 | [res.id]: normalizeResult(res) 129 | } 130 | resultIds = [...resultIds, res.id] 131 | }) 132 | 133 | return { 134 | ...state, 135 | resultsById, 136 | resultIds: orderBy(uniq(resultIds), id => resultsById[id].order || 0) 137 | } 138 | } 139 | case CHANGE_VISIBLE_RESULTS: { 140 | return { 141 | ...state, 142 | visibleResults: payload, 143 | } 144 | } 145 | case RESET: { 146 | return { 147 | // Do not override last used search term with empty string 148 | ...state, 149 | prevTerm: state.term || state.prevTerm, 150 | resultsById: {}, 151 | resultIds: [], 152 | term: '', 153 | selected: 0, 154 | } 155 | } 156 | default: 157 | return state 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/main/actions/search.js: -------------------------------------------------------------------------------- 1 | import plugins from '../plugins/' 2 | import config from 'lib/config' 3 | import { shell, clipboard, remote } from 'electron' 4 | import store from '../store' 5 | import { settings as pluginSettings } from 'lib/plugins' 6 | 7 | import { 8 | UPDATE_TERM, 9 | MOVE_CURSOR, 10 | SELECT_ELEMENT, 11 | SHOW_RESULT, 12 | HIDE_RESULT, 13 | UPDATE_RESULT, 14 | RESET, 15 | CHANGE_VISIBLE_RESULTS, 16 | } from '../constants/actionTypes' 17 | 18 | /** 19 | * Default scope object would be first argument for plugins 20 | * 21 | * @type {Object} 22 | */ 23 | const DEFAULT_SCOPE = { 24 | config, 25 | actions: { 26 | open: (q) => shell.openExternal(q), 27 | reveal: (q) => shell.showItemInFolder(q), 28 | copyToClipboard: (q) => clipboard.writeText(q), 29 | replaceTerm: (term) => store.dispatch(updateTerm(term)), 30 | hideWindow: () => remote.getCurrentWindow().hide() 31 | } 32 | } 33 | 34 | /** 35 | * Pass search term to all plugins and handle their results 36 | * @param {String} term Search term 37 | * @param {Function} callback Callback function that receives used search term and found results 38 | */ 39 | const eachPlugin = (term, display) => { 40 | // TODO: order results by frequency? 41 | Object.keys(plugins).forEach(name => { 42 | try { 43 | plugins[name].fn({ 44 | ...DEFAULT_SCOPE, 45 | term, 46 | hide: (id) => store.dispatch(hideElement(`${name}-${id}`)), 47 | update: (id, result) => store.dispatch(updateElement(`${name}-${id}`, result)), 48 | display: (payload) => display(name, payload), 49 | settings: pluginSettings.get(name), 50 | }) 51 | } catch (error) { 52 | // Do not fail on plugin errors, just log them to console 53 | console.log('Error running plugin', name, error) 54 | } 55 | }) 56 | } 57 | 58 | 59 | /** 60 | * Handle results found by plugin 61 | * 62 | * @param {String} term Search term that was used for found results 63 | * @param {Array or Object} result Found results (or result) 64 | * @return {Object} redux action 65 | */ 66 | function onResultFound(term, result) { 67 | return { 68 | type: SHOW_RESULT, 69 | payload: { 70 | result, 71 | term, 72 | } 73 | } 74 | } 75 | 76 | 77 | /** 78 | * Action that clears everthing in search box 79 | * 80 | * @return {Object} redux action 81 | */ 82 | export function reset() { 83 | return { 84 | type: RESET, 85 | } 86 | } 87 | 88 | /** 89 | * Action that updates search term 90 | * 91 | * @param {String} term 92 | * @return {Object} redux action 93 | */ 94 | export function updateTerm(term) { 95 | if (term === '') { 96 | return reset() 97 | } 98 | return (dispatch) => { 99 | dispatch({ 100 | type: UPDATE_TERM, 101 | payload: term, 102 | }) 103 | eachPlugin(term, (plugin, payload) => { 104 | let result = Array.isArray(payload) ? payload : [payload] 105 | result = result.map(x => ({ 106 | ...x, 107 | plugin, 108 | // Scope result ids with plugin name and use title if id is empty 109 | id: `${plugin}-${x.id || x.title}` 110 | })) 111 | if (result.length === 0) { 112 | // Do not dispatch for empty results 113 | return 114 | } 115 | dispatch(onResultFound(term, result)) 116 | }) 117 | } 118 | } 119 | 120 | /** 121 | * Action to move highlighted cursor to next or prev element 122 | * @param {Integer} diff 1 or -1 123 | * @return {Object} redux action 124 | */ 125 | export function moveCursor(diff) { 126 | return { 127 | type: MOVE_CURSOR, 128 | payload: diff 129 | } 130 | } 131 | 132 | /** 133 | * Action to change highlighted element 134 | * @param {Integer} index of new highlighted element 135 | * @return {Object} redux action 136 | */ 137 | export function selectElement(index) { 138 | return { 139 | type: SELECT_ELEMENT, 140 | payload: index 141 | } 142 | } 143 | 144 | /** 145 | * Action to remove element from results list by id 146 | * @param {String} id 147 | * @return {Object} redux action 148 | */ 149 | export function hideElement(id) { 150 | return { 151 | type: HIDE_RESULT, 152 | payload: { id } 153 | } 154 | } 155 | 156 | /** 157 | * Action to update displayed element with new result 158 | * @param {String} id 159 | * @return {Object} redux action 160 | */ 161 | export function updateElement(id, result) { 162 | return { 163 | type: UPDATE_RESULT, 164 | payload: { id, result } 165 | } 166 | } 167 | 168 | /** 169 | * Change count of visible results (without scroll) in list 170 | */ 171 | export function changeVisibleResults(count) { 172 | return { 173 | type: CHANGE_VISIBLE_RESULTS, 174 | payload: count, 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/main/plugins/core/plugins/Preview/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { KeyboardNav, KeyboardNavItem, Preload } from 'cerebro-ui' 3 | import ActionButton from './ActionButton.js' 4 | import Settings from './Settings' 5 | import getReadme from '../getReadme' 6 | import ReactMarkdown from 'react-markdown' 7 | import styles from './styles.css' 8 | import trackEvent from 'lib/trackEvent' 9 | import * as format from '../format' 10 | import { client } from 'lib/plugins' 11 | import plugins from 'main/plugins' 12 | 13 | const isRelative = (src) => !src.match(/^(https?:|data:)/) 14 | const urlTransform = (repo, src) => { 15 | if (isRelative(src)) { 16 | return `http://raw.githubusercontent.com/${repo}/master/${src}` 17 | } 18 | return src 19 | } 20 | 21 | class Preview extends Component { 22 | constructor(props) { 23 | super(props) 24 | this.onComplete = this.onComplete.bind(this) 25 | this.state = { 26 | showDescription: false, 27 | showSettings: false, 28 | } 29 | } 30 | 31 | onComplete() { 32 | this.setState({ runningAction: null }) 33 | this.props.onComplete() 34 | } 35 | 36 | pluginAction(plugin, runningAction) { 37 | return () => { 38 | this.setState({ runningAction }) 39 | trackEvent({ 40 | category: 'Plugins', 41 | event: runningAction, 42 | label: plugin 43 | }) 44 | client[runningAction](plugin) 45 | } 46 | } 47 | 48 | renderDescription(repo) { 49 | return ( 50 | 51 | { 52 | (content) => ( 53 | urlTransform(repo, src)} 57 | /> 58 | ) 59 | } 60 | 61 | ) 62 | } 63 | 64 | render() { 65 | const { 66 | name, 67 | version, 68 | description, 69 | repo, 70 | isInstalled, 71 | installedVersion, 72 | isUpdateAvailable 73 | } = this.props 74 | const githubRepo = repo && repo.match(/^.+github.com\/([^\/]+\/[^\/]+).*?/) 75 | const { runningAction, showSettings } = this.state 76 | const settings = plugins[name] ? plugins[name].settings : null 77 | return ( 78 |
79 |

{format.name(name)} ({version})

80 |

{format.description(description)}

81 | 82 |
83 | { 84 | settings && 85 | this.setState({ showSettings: !this.state.showSettings })} 87 | > 88 | Settings 89 | 90 | } 91 | {showSettings && } 92 | { 93 | !isInstalled && 94 | 99 | } 100 | { 101 | isInstalled && 102 | 107 | } 108 | { 109 | isUpdateAvailable && 110 | 119 | } 120 | { 121 | githubRepo && 122 | this.setState({ showDescription: !this.state.showDescription })} 124 | > 125 | Details 126 | 127 | } 128 |
129 |
130 | {this.state.showDescription && this.renderDescription(githubRepo[1])} 131 |
132 | ) 133 | } 134 | } 135 | 136 | Preview.propTypes = { 137 | name: PropTypes.string.isRequired, 138 | settings: PropTypes.object, 139 | version: PropTypes.string.isRequired, 140 | description: PropTypes.string, 141 | repo: PropTypes.string, 142 | installedVersion: PropTypes.string, 143 | isInstalled: PropTypes.bool.isRequired, 144 | isUpdateAvailable: PropTypes.bool.isRequired, 145 | onComplete: PropTypes.func.isRequired, 146 | } 147 | 148 | export default Preview 149 | -------------------------------------------------------------------------------- /app/main/createWindow.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, globalShortcut, app, screen, shell } from 'electron' 2 | import debounce from 'lodash/debounce' 3 | import EventEmitter from 'events' 4 | import trackEvent from '../lib/trackEvent' 5 | 6 | import { 7 | INPUT_HEIGHT, 8 | WINDOW_WIDTH 9 | } from './constants/ui' 10 | 11 | import buildMenu from './createWindow/buildMenu' 12 | import toggleWindow from './createWindow/toggleWindow' 13 | import handleUrl from './createWindow/handleUrl' 14 | import config from '../lib/config' 15 | import getWindowPosition from '../lib/getWindowPosition' 16 | import * as donateDialog from './createWindow/donateDialog' 17 | 18 | export default ({ src, isDev }) => { 19 | const [x, y] = getWindowPosition({}) 20 | 21 | const browserWindowOptions = { 22 | width: WINDOW_WIDTH, 23 | minWidth: WINDOW_WIDTH, 24 | height: INPUT_HEIGHT, 25 | x, 26 | y, 27 | frame: false, 28 | resizable: false, 29 | // Show main window on launch only when application started for the first time 30 | show: config.get('firstStart') 31 | } 32 | 33 | if (process.platform === 'linux') { 34 | browserWindowOptions.type = 'splash' 35 | } 36 | 37 | const mainWindow = new BrowserWindow(browserWindowOptions) 38 | 39 | // Float main window above full-screen apps 40 | mainWindow.setAlwaysOnTop(true, 'modal-panel') 41 | 42 | mainWindow.loadURL(src) 43 | mainWindow.settingsChanges = new EventEmitter() 44 | 45 | // Get global shortcut from app settings 46 | let shortcut = config.get('hotkey') 47 | 48 | // Function to toggle main window 49 | const toggleMainWindow = () => toggleWindow(mainWindow) 50 | // Function to show main window 51 | const showMainWindow = () => { 52 | mainWindow.show() 53 | mainWindow.focus() 54 | } 55 | 56 | // Setup event listeners for main window 57 | globalShortcut.register(shortcut, toggleMainWindow) 58 | 59 | mainWindow.on('blur', () => { 60 | if (!isDev()) { 61 | // Hide window on blur in production 62 | // In development we usually use developer tools that can blur a window 63 | mainWindow.hide() 64 | } 65 | }) 66 | 67 | // Save window position when it is being moved 68 | mainWindow.on('move', debounce(() => { 69 | if (!mainWindow.isVisible()) { 70 | return 71 | } 72 | const display = screen.getPrimaryDisplay() 73 | const positions = config.get('positions') || {} 74 | positions[display.id] = mainWindow.getPosition() 75 | config.set('positions', positions) 76 | }, 100)) 77 | 78 | mainWindow.on('close', app.quit) 79 | 80 | mainWindow.webContents.on('new-window', (event, url) => { 81 | shell.openExternal(url) 82 | event.preventDefault() 83 | }) 84 | 85 | mainWindow.webContents.on('will-navigate', (event, url) => { 86 | if (url !== mainWindow.webContents.getURL()) { 87 | shell.openExternal(url) 88 | event.preventDefault() 89 | } 90 | }) 91 | 92 | // Change global hotkey if it is changed in app settings 93 | mainWindow.settingsChanges.on('hotkey', (value) => { 94 | globalShortcut.unregister(shortcut) 95 | shortcut = value 96 | globalShortcut.register(shortcut, toggleMainWindow) 97 | }) 98 | 99 | // Change theme css file 100 | mainWindow.settingsChanges.on('theme', (value) => { 101 | mainWindow.webContents.send('message', { 102 | message: 'updateTheme', 103 | payload: value 104 | }) 105 | }) 106 | 107 | // Handle window.hide: if cleanOnHide value in preferences is true 108 | // we clear all results and show empty window every time 109 | const resetResults = () => { 110 | mainWindow.webContents.send('message', { 111 | message: 'showTerm', 112 | payload: '' 113 | }) 114 | } 115 | 116 | // Handle change of cleanOnHide value in settins 117 | const handleCleanOnHideChange = (value) => { 118 | if (value) { 119 | mainWindow.on('hide', resetResults) 120 | } else { 121 | mainWindow.removeListener('hide', resetResults) 122 | } 123 | } 124 | 125 | // Set or remove handler when settings changed 126 | mainWindow.settingsChanges.on('cleanOnHide', handleCleanOnHideChange) 127 | 128 | // Set initial handler if it is needed 129 | handleCleanOnHideChange(config.get('cleanOnHide')) 130 | 131 | // Show main window when user opens application, but it is already opened 132 | app.on('open-file', (event, path) => handleUrl(mainWindow, path)) 133 | app.on('open-url', (event, path) => handleUrl(mainWindow, path)) 134 | app.on('activate', showMainWindow) 135 | 136 | // Someone tried to run a second instance, we should focus our window. 137 | const shouldQuit = app.makeSingleInstance(() => { 138 | if (mainWindow) { 139 | if (mainWindow.isMinimized()) mainWindow.restore() 140 | mainWindow.focus() 141 | } 142 | }) 143 | 144 | if (shouldQuit) { 145 | app.quit() 146 | } 147 | 148 | if (donateDialog.shouldShow()) { 149 | setTimeout(donateDialog.show, 1000) 150 | } 151 | 152 | // Track app start event 153 | trackEvent({ 154 | category: 'App Start', 155 | event: config.get('firstStart') ? 'First' : 'Secondary' 156 | }) 157 | 158 | // Save in config information, that application has been started 159 | config.set('firstStart', false) 160 | 161 | buildMenu(mainWindow) 162 | return mainWindow 163 | } 164 | -------------------------------------------------------------------------------- /app/lib/plugins/npm.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import os from 'os' 3 | import rmdir from 'rmdir' 4 | import path from 'path' 5 | import tar from 'tar-fs' 6 | import zlib from 'zlib' 7 | import https from 'https' 8 | import mv from 'mv' 9 | 10 | /** 11 | * Promise-wrapper for rmdir 12 | * @param {String} dir 13 | * @return {Promise} 14 | */ 15 | const removeDir = (dir) => new Promise((resolve, reject) => { 16 | rmdir(dir, err => err ? reject(err) : resolve()) 17 | }) 18 | 19 | /** 20 | * Base url of npm API 21 | * 22 | * @type {String} 23 | */ 24 | const API_BASE = 'http://registry.npmjs.org/' 25 | 26 | /** 27 | * Format name of file from package archive. 28 | * Just remove `./package`prefix from name 29 | * 30 | * @param {Object} header 31 | * @return {Object} 32 | */ 33 | const formatPackageFile = (header) => ({ 34 | ...header, 35 | name: header.name.replace(/^package\//, '') 36 | }) 37 | 38 | const installPackage = (tarPath, destination, middleware) => { 39 | console.log(`Extract ${tarPath} to ${destination}`) 40 | return new Promise((resolve, reject) => { 41 | const packageName = path.parse(destination).name 42 | const tempPath = `${os.tmpdir()}/${packageName}` 43 | console.log(`Download and extract to temp path: ${tempPath}`) 44 | https.get(tarPath, stream => { 45 | const result = stream 46 | // eslint-disable-next-line new-cap 47 | .pipe(zlib.Unzip()) 48 | .pipe(tar.extract(tempPath, { 49 | map: formatPackageFile 50 | })) 51 | result.on('error', reject) 52 | result.on('finish', () => { 53 | middleware().then(() => { 54 | console.log(`Move ${tempPath} to ${destination}`) 55 | mv(tempPath, destination, (err) => err ? reject(err) : resolve()) 56 | }) 57 | }) 58 | }) 59 | }) 60 | } 61 | 62 | /** 63 | * Lightweight npm client. 64 | * It only can install/uninstall package, without resolving dependencies 65 | * 66 | * @param {String} path Path to npm package directory 67 | * @return {Object} 68 | */ 69 | export default (dir) => { 70 | const packageJson = path.join(dir, 'package.json') 71 | const setConfig = (config) => ( 72 | fs.writeFileSync(packageJson, JSON.stringify(config, null, 2)) 73 | ) 74 | const getConfig = () => JSON.parse(fs.readFileSync(packageJson)) 75 | return { 76 | /** 77 | * Install npm package 78 | * @param {String} name Name of package in npm registry 79 | * 80 | * @param {Object} options 81 | * version {String} Version of npm package. Default is latest version 82 | * middleware {Function} 83 | * Function that returns promise. Called when package's archive is extracted 84 | * to temp folder, but before moving to real location 85 | * @return {Promise} 86 | */ 87 | install(name, options = {}) { 88 | let versionToInstall 89 | const version = options.version || null 90 | const middleware = options.middleware || (() => Promise.resolve()) 91 | console.group('[npm] Install package', name) 92 | return fetch(`${API_BASE}${name}`) 93 | .then(response => response.json()) 94 | .then(json => { 95 | versionToInstall = version || json['dist-tags'].latest 96 | console.log('Version:', versionToInstall) 97 | return installPackage( 98 | json.versions[versionToInstall].dist.tarball, 99 | path.join(dir, 'node_modules', name), 100 | middleware 101 | ) 102 | }) 103 | .then(() => { 104 | const json = getConfig() 105 | json.dependencies[name] = versionToInstall 106 | console.log('Add package to dependencies') 107 | setConfig(json) 108 | console.groupEnd() 109 | }) 110 | .catch(err => { 111 | console.log('Error in package installation') 112 | console.log(err) 113 | console.groupEnd() 114 | }) 115 | }, 116 | update(name) { 117 | // Plugin update is downloading `.tar` and unarchiving it to temp folder 118 | // Only if this part was succeeded, current version of plugin is uninstalled 119 | // and temp folder moved to real plugin location 120 | const middleware = () => this.uninstall(name) 121 | return this.install(name, { middleware }) 122 | }, 123 | /** 124 | * Uninstall npm package 125 | * 126 | * @param {String} name 127 | * @return {Promise} 128 | */ 129 | uninstall(name) { 130 | const modulePath = path.join(dir, 'node_modules', name) 131 | console.group('[npm] Uninstall package', name) 132 | console.log('Remove package directory ', modulePath) 133 | return removeDir(modulePath) 134 | .then(() => { 135 | const json = getConfig() 136 | console.log('Update package.json') 137 | json.dependencies = Object 138 | .keys(json.dependencies || {}) 139 | .reduce((acc, key) => { 140 | if (key !== name) { 141 | return { 142 | ...acc, 143 | [key]: json.dependencies[key] 144 | } 145 | } 146 | return acc 147 | }, {}) 148 | console.log('Rewrite package.json') 149 | setConfig(json) 150 | console.groupEnd() 151 | return true 152 | }) 153 | .catch(err => { 154 | console.log('Error in package uninstallation') 155 | console.log(err) 156 | console.groupEnd() 157 | }) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /docs/plugins/plugin-structure.md: -------------------------------------------------------------------------------- 1 | # Plugins → Plugin structure 2 | 3 | This is a minimum source code of your plugin: 4 | 5 | ```js 6 | module.exports = { 7 | fn: (scope) => console.log(scope.term) 8 | } 9 | ``` 10 | 11 | This plugin will write to console all changes in your search field of Cerebro app. So, `fn` key is a heart of your plugin: this function receives `scope` object and your can send results back to Cerebro. Scope object is: 12 | 13 | * `term` – `String`, entered by Cerebro user; 14 | * `display` – `Function(result: Object | Array)`, display your result 15 | * `update` – `Function(id: String, result: Object)`, update your previously displayed result. This action updates only passed fields, so if you displayed result `{id: 1, title: 'Result'}` and call `update(1, {subtitle: 'Subtitle'})`, you will have merged result: `{id: 1, title: 'Result', subtitle: 'Subtitle'}`; 16 | * `hide` – `Function(id: String)`, hide result from results list by id. You can use it to remove temporar results, like "loading..." placeholder; 17 | * `actions` – object with main actions, provided for cerebro plugins: 18 | * `open` – `Function(path: String)`, open external URL in browser or open local file; 19 | * `reveal` – `Function(path: String)`, reveal file in finder; 20 | * `copyToClipboard` – `Function(text: String)`, copy text to clipboard; 21 | * `replaceTerm` – `Function(text: String)`, replace text in main Cerebro input; 22 | * `hideWindow` – `Function()`, hide main Cerebro window. 23 | * `settings` - `Object`, contains user provided values of all specified settings keys; 24 | 25 | 26 | Let's show something in results list: 27 | 28 | ```js 29 | const plugin = (scope) => { 30 | scope.display({ 31 | title: 'It works!', 32 | subtitle: `You entered ${scope.term}` 33 | }) 34 | } 35 | 36 | module.exports = { 37 | fn: plugin 38 | } 39 | ``` 40 | 41 | `scope.display` accepts one or several results. Result object is: 42 | 43 | ## Basic fields 44 | ### `title` 45 | Type: `String` 46 | 47 | Title of your result; 48 | 49 | ### `subtitle` 50 | Type: `String` 51 | 52 | Subtitle of your result; 53 | 54 | ### `icon` 55 | Type: `String` 56 | 57 | Icon, that is shown near your result. It can be absolute URL to external image, absolute path to local image or base64-encoded [data-uri](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). 58 | 59 | For local icons you can use path to some `.app` file, i.e. `/Applications/Calculator.app` will render default icon for Calculator application. 60 | 61 | ## Advanced fields 62 | 63 | ### `id` 64 | Type: `String` 65 | Use this field when you need to update your result dynamically. Check `id` [example](./examples.md#using-id) 66 | 67 | ### `term` 68 | Type: `String` 69 | 70 | Autocomplete for your result. So, user can update search term using tab button; 71 | 72 | ### `clipboard` 73 | Type: `String` 74 | 75 | Text, that will be copied to clipboard using cmd+c, when your result is focused; 76 | 77 | ### `getPreview` 78 | Type: `Function` 79 | 80 | Arguments: no 81 | 82 | Function that returns preview for your result. Preview can be an html string or React component; 83 | 84 | ### `onSelect` 85 | Type: `Function`. 86 | Arguments: `event: Event` 87 | 88 | Action, that should be executed when user selects your result. I.e, to open provided url in default browser: 89 | 90 | ``` 91 | onSelect: (event) => actions.open(`http://www.cerebroapp.com`), 92 | ``` 93 | 94 | If you don't want to close main window after your action, you should call `event.preventDefault()` in your action. 95 | 96 | ### `onKeyDown` 97 | Type: `Function` 98 | 99 | Arguments: `event: Event` 100 | 101 | Handle keyboard events when your result is focused, so you can do custom actions, i.e. reveal file in finder by cmd+r (or ctrl+r on windows and linux): 102 | 103 | ```js 104 | onKeyDown: (event) => { 105 | if ((event.metaKey || event.ctrlKey) && event.keyCode === 82) { 106 | actions.reveal(path); 107 | event.preventDefault(); 108 | } 109 | } 110 | ``` 111 | 112 | You can also prevent default action by `event.preventDefault()`. 113 | 114 | ## Advanced plugin fields 115 | Along with `fn`, your module could have more keys: 116 | 117 | ### `keyword` 118 | Type: `String` 119 | 120 | This field is used for autocomplete. You can prefix your plugin usage by this keyword. Checkout emoji [example](./examples.md#using-keyword-and-name) 121 | 122 | ### `name` 123 | Type: `String` 124 | 125 | This field is also used for autocomplete and shown as title in results list. Checkout emoji [example](./examples.md#using-keyword-and-name) 126 | 127 | ### `initialize` 128 | Type: `Function` 129 | Arguments: no 130 | 131 | Use this function, when you need to prepare some data for your plugin on start. If you need to do some long-running processes, check `initializeAsync` 132 | 133 | Check `initialize` [example](./examples.md#using-initialize) 134 | 135 | ### `initializeAsync` 136 | Type: `Function` 137 | 138 | Arguments: `callback: Function(message: Object)` – callback to send data back to main process. 139 | 140 | Use this function when you need to execute some long-running initializer process. I.e. in contacts plugin we are fetching all contacts using osx-specific libraries using `nodobjc` module. 141 | 142 | This function will be executed in another process and you can receive results using `onMessage` function. 143 | 144 | Check `initializeAsync` and `onMessage` [example](./examples.md#using-initializeasync-and-onmessage) 145 | 146 | ### `onMessage` 147 | Type: `Function` 148 | Arguments: `message: Object` – object that you sent from `initializeAsync` 149 | 150 | Use this function to receive data back from your `initializeAsync` function. 151 | 152 | Check `initializeAsync` and `onMessage` [example](./examples.md#using-initializeasync-and-onmessage) 153 | 154 | ### `settings` 155 | Type: `Object` 156 | 157 | This object is used to specify settings that a plugin user can change. Each setting should include a `description` and a `type`. Other keys include: 158 | * `label` - `String`, object key for the setting. also used to access it; 159 | * `description` - `String`, description of the setting; 160 | * `type` - `String`, used to decide element for rendering a setting: 161 | * `string` 162 | * `number` 163 | * `bool` 164 | * `option` 165 | * `defaultValue` - `Any`, default value for the setting; 166 | * `options` - `Array`, all possible options that can be selected by the user. applicable only for `option`; 167 | * `multi` - `Bool`, allows user to select more than one option for `option` settings. applicable only for `option`; 168 | * `createable` - `Bool`, allows user created options. applicable only for `option`; 169 | 170 | Check `settings` [example](./examples.md#using-settings) 171 | 172 | Look at [React Select](https://github.com/JedWatson/react-select) for more details on how the `option` type works. 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerebro", 3 | "productName": "Cerebro", 4 | "version": "0.2.8", 5 | "description": "Extendable electron-based open-source Spotlight and Alfred analogue", 6 | "main": "main.js", 7 | "scripts": { 8 | "test": "cross-env NODE_ENV=test ./node_modules/.bin/mocha-webpack", 9 | "test-watch": "yarn run test -- --watch", 10 | "lint": "eslint app/background app/lib app/main test *.js", 11 | "hot-server": "node -r babel-register server.js", 12 | "build-main": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress --profile --colors", 13 | "build-main-dev": "cross-env NODE_ENV=development node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress --profile --colors", 14 | "build-renderer": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.production.js --progress --profile --colors", 15 | "bundle-analyze": "cross-env ANALYZE=true NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.production.js --progress --profile --colors && open ./app/dist/stats.html", 16 | "build": "yarn run build-main && yarn run build-renderer", 17 | "start": "cross-env NODE_ENV=production electron ./app", 18 | "start-hot": "yarn build-main-dev && cross-env HOT=1 NODE_ENV=development ./node_modules/.bin/electron -r babel-register ./app", 19 | "package": "yarn run build && build --publish never", 20 | "release": "build -mwl --draft", 21 | "dev": "concurrently --kill-others \"yarn run hot-server\" \"yarn run start-hot\"", 22 | "postinstall": "concurrently \"node node_modules/fbjs-scripts/node/check-dev-engines.js package.json\"" 23 | }, 24 | "build": { 25 | "productName": "Cerebro", 26 | "appId": "com.cerebroapp.Cerebro", 27 | "protocols": { 28 | "name": "Cerebro URLs", 29 | "role": "Viewer", 30 | "schemes": [ 31 | "cerebro" 32 | ] 33 | }, 34 | "directories": { 35 | "app": "./app", 36 | "output": "release" 37 | }, 38 | "linux": { 39 | "target": [ 40 | { 41 | "target": "deb", 42 | "arch": [ 43 | "x64" 44 | ] 45 | }, 46 | { 47 | "target": "AppImage", 48 | "arch": [ 49 | "x64" 50 | ] 51 | } 52 | ], 53 | "category": "public.app-category.productivity" 54 | }, 55 | "mac": { 56 | "category": "public.app-category.productivity" 57 | }, 58 | "dmg": { 59 | "contents": [ 60 | { 61 | "x": 410, 62 | "y": 150, 63 | "type": "link", 64 | "path": "/Applications" 65 | }, 66 | { 67 | "x": 130, 68 | "y": 150, 69 | "type": "file" 70 | } 71 | ] 72 | }, 73 | "win": { 74 | "target": "nsis" 75 | }, 76 | "nsis": { 77 | "include": "build/installer.nsh", 78 | "perMachine": true 79 | }, 80 | "files": [ 81 | "dist/", 82 | "main/index.html", 83 | "main/css,", 84 | "background/index.html", 85 | "tray_icon.png", 86 | "tray_icon.ico", 87 | "tray_iconTemplate@2x.png", 88 | "node_modules/", 89 | "main.js", 90 | "main.js.map", 91 | "package.json", 92 | "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}", 93 | "!**/node_modules/.bin", 94 | "!**/*.{o,hprof,orig,pyc,pyo,rbc}", 95 | "!**/{.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}" 96 | ], 97 | "squirrelWindows": { 98 | "iconUrl": "https://raw.githubusercontent.com/KELiON/cerebro/master/build/icon.ico" 99 | }, 100 | "publish": { 101 | "provider": "github", 102 | "vPrefixedTagName": false 103 | } 104 | }, 105 | "bin": { 106 | "electron": "./node_modules/.bin/electron" 107 | }, 108 | "repository": { 109 | "type": "git", 110 | "url": "git+https://github.com/KELiON/cerebro.git" 111 | }, 112 | "author": { 113 | "name": "Alexandr Subbotin", 114 | "email": "kelionweb@gmail.com", 115 | "url": "https://github.com/KELiON" 116 | }, 117 | "license": "MIT", 118 | "bugs": { 119 | "url": "https://github.com/KELiON/cerebro/issues" 120 | }, 121 | "keywords": [ 122 | "launcher", 123 | "electron", 124 | "alfred", 125 | "spotlight" 126 | ], 127 | "homepage": "https://cerebroapp.com", 128 | "devDependencies": { 129 | "asar": "0.13.0", 130 | "autoprefixer": "6.7.7", 131 | "babel-core": "^6.24.1", 132 | "babel-eslint": "7.2.3", 133 | "babel-loader": "6.4.1", 134 | "babel-plugin-add-module-exports": "0.2.1", 135 | "babel-plugin-lodash": "3.2.11", 136 | "babel-plugin-transform-class-properties": "6.24.1", 137 | "babel-plugin-transform-decorators-legacy": "1.3.4", 138 | "babel-plugin-transform-object-rest-spread": "6.23.0", 139 | "babel-plugin-transform-remove-debugger": "^6.8.1", 140 | "babel-polyfill": "6.23.0", 141 | "babel-preset-env": "^1.4.0", 142 | "babel-preset-react": "6.24.1", 143 | "babel-preset-react-hmre": "1.1.1", 144 | "babel-preset-react-optimize": "1.0.1", 145 | "babel-register": "^6.24.1", 146 | "chai": "3.5.0", 147 | "chai-spies": "0.7.1", 148 | "chromedriver": "2.29.0", 149 | "co-mocha": "1.2.0", 150 | "concurrently": "3.4.0", 151 | "copy-webpack-plugin": "4.0.1", 152 | "cross-env": "1.0.8", 153 | "css-loader": "^0.28.1", 154 | "del": "2.2.2", 155 | "devtron": "1.4.0", 156 | "electron": "1.6.10", 157 | "electron-builder": "17.10.0", 158 | "electron-rebuild": "1.5.11", 159 | "eslint": "^3.19.0", 160 | "eslint-config-airbnb": "9.0.1", 161 | "eslint-import-resolver-webpack": "^0.8.1", 162 | "eslint-plugin-import": "^2.2.0", 163 | "eslint-plugin-jsx-a11y": "1.2.2", 164 | "eslint-plugin-react": "5.1.1", 165 | "expect": "1.20.2", 166 | "express": "4.13.4", 167 | "extract-text-webpack-plugin": "2.1.0", 168 | "fbjs-scripts": "0.7.1", 169 | "file-loader": "0.11.1", 170 | "inject-loader": "3.0.0", 171 | "jsdom": "9.2.0", 172 | "json-loader": "0.5.4", 173 | "lodash-webpack-plugin": "0.10.6", 174 | "minimist": "1.2.0", 175 | "mocha": "3.3.0", 176 | "mocha-webpack": "0.7.0", 177 | "node-libs-browser": "2.0.0", 178 | "null-loader": "0.1.1", 179 | "optimize-js-plugin": "0.0.4", 180 | "postcss-loader": "^2.0.5", 181 | "postcss-nested": "1.0.1", 182 | "raw-loader": "0.5.1", 183 | "react-addons-test-utils": "^15.4.2", 184 | "redux-devtools": "3.3.1", 185 | "redux-devtools-dock-monitor": "1.1.1", 186 | "redux-devtools-log-monitor": "1.0.11", 187 | "redux-logger": "2.6.1", 188 | "selenium-webdriver": "3.3.0", 189 | "sinon": "2.2.0", 190 | "style-loader": "^0.17.0", 191 | "url-loader": "https://registry.npmjs.org/url-loader/-/url-loader-0.5.7.tgz", 192 | "webpack": "2.5.1", 193 | "webpack-dev-middleware": "1.6.1", 194 | "webpack-hot-middleware": "2.10.0", 195 | "webpack-visualizer-plugin": "0.1.5" 196 | }, 197 | "dependencies": { 198 | "cerebro-tools": "0.1.8", 199 | "cerebro-ui": "0.0.13", 200 | "co": "4.6.0", 201 | "escape-string-regexp": "1.0.5", 202 | "fix-path": "2.1.0", 203 | "glob": "7.0.5", 204 | "lodash": "4.13.1", 205 | "lodash-decorators": "3.0.1", 206 | "mv": "^2.1.1", 207 | "node-machine-id": "1.1.4", 208 | "normalize.css": "4.1.1", 209 | "postcss": "5.2.17", 210 | "react": "15.4.2", 211 | "react-addons-css-transition-group": "^15.4.2", 212 | "react-addons-shallow-compare": "^15.4.2", 213 | "react-dom": "15.4.2", 214 | "react-input-autosize": "^1.1.0", 215 | "react-markdown": "2.4.2", 216 | "react-redux": "4.4.5", 217 | "react-virtualized": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-7.11.8.tgz", 218 | "redux": "3.5.2", 219 | "redux-thunk": "2.1.0", 220 | "source-map-support": "0.4.0", 221 | "tar-fs": "1.15.0" 222 | }, 223 | "devEngines": { 224 | "node": ">=6.x" 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cerebro 2 | 3 | 4 | 5 | [![Build Status][travis-image]][travis-url] 6 | [![Dependency Status][david_img]][david_site] 7 | [![OpenCollective](https://opencollective.com/cerebro/backers/badge.svg)](#backers) 8 | [![OpenCollective](https://opencollective.com/cerebro/sponsors/badge.svg)](#sponsors) 9 | 10 | ## Usage 11 | You can download the latest version on [releases](https://github.com/KELiON/cerebro/releases) page. 12 | 13 | After installation use default shortcut `ctrl+space` to show an app window. You can customize this shortcut clicking on icon in menu bar → preferences. 14 | 15 | ![Cerebro](https://cloud.githubusercontent.com/assets/594298/20180624/858a483a-a75b-11e6-94a1-ef1edc4d95c3.gif) 16 | 17 | ## Plugins 18 | ### Core plugins 19 | * Search in the web with google suggestions; 20 | * Search & launch application, i.e. `spotify`; 21 | * Navigate in file system with file previews (i.e. `~/Dropbox/passport.pdf`); 22 | * Calculator; 23 | * Smart converter. `15$`, `150 рублей в евро`, `100 eur in gbp`; 24 | 25 | ### Install and manage custom plugins 26 | Use built-in `plugins` command to search and manage custom plugins. 27 | 28 | Discover plugins and more at [Cerebro's Awesome List](https://github.com/lubien/awesome-cerebro). 29 | 30 | ## Development 31 | 32 | If you have any questions feel free to chat in gitter: https://gitter.im/KELiON-cerebro. 33 | 34 | ### Create plugin 35 | Check out [plugins documentation](./docs/plugins.md). 36 | 37 | ### Install 38 | 39 | First, clone the repo via git: 40 | 41 | ```bash 42 | $ git clone https://github.com/KELiON/cerebro.git cerebro 43 | ``` 44 | 45 | And then install dependencies: 46 | 47 | ```bash 48 | $ cd cerebro && yarn && cd ./app && yarn && cd ../ 49 | ``` 50 | 51 | ### Run 52 | ```bash 53 | $ yarn run dev 54 | ``` 55 | 56 | > Note: requires a node version >=6.x 57 | 58 | ### Resolve common issues 59 | 1. `AssertionError: Current node version is not supported for development` on npm postinstall. 60 | After `yarn` postinstall script checks node version. If you see this error you have to check node and npm version in `package.json` `devEngines` section and install proper ones. 61 | 62 | 2. `Uncaught Error: Module version mismatch. Exepcted 50, got ...` 63 | This error means that node modules with native extensions build with wrong node version (your local node version != node version, included to electron). To fix this issue run `cd ./app && yarn run rebuild` 64 | 65 | #### Config file path 66 | 67 | 68 | *WINDOWS*: `%APPDATA%/Cerebro/config.json` 69 | 70 | *Linux*: `$XDG_CONFIG_HOME/Cerebro/config.json` or `~/.config/Cerebro/config.json` 71 | 72 | *Mac OS*: `~/Library/Application Support/Cerebro/config.json` 73 | 74 | 75 | ### Package 76 | Use this command to build `.app` file: 77 | 78 | ```bash 79 | $ yarn run package 80 | ``` 81 | 82 | 83 | ## Be in touch 84 | Follow to be notified about new releases or learn some productivity tips with Cerebro: 85 | 86 | * [Twitter](https://twitter.com/cerebro_app) 87 | * [Facebook](https://www.facebook.com/cerebroapp) 88 | * [Google+](https://plus.google.com/104292436165594177472) 89 | * [VK.com](https://vk.com/cerebroapp) – channel in Russian 90 | 91 | Or [subscribe to newsletter](http://eepurl.com/coiKU9) to be notified only about big releases. 92 | 93 | ## Support 94 | ### Backers 95 | Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/cerebro#backer)] 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | ### Sponsors 129 | Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/cerebro#sponsor)] 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | ## License 163 | MIT © [Alexandr Subbotin](https://github.com/KELiON) 164 | 165 | [travis-image]: https://travis-ci.org/KELiON/cerebro.svg?branch=master 166 | [travis-url]: https://travis-ci.org/KELiON/cerebro 167 | [david_img]: https://img.shields.io/david/KELiON/cerebro.svg 168 | [david_site]: https://david-dm.org/KELiON/cerebro 169 | -------------------------------------------------------------------------------- /app/main/containers/Search/index.js: -------------------------------------------------------------------------------- 1 | /* eslint default-case: 0 */ 2 | 3 | import React, { Component, PropTypes } from 'react' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | import { clipboard, remote } from 'electron' 7 | import MainInput from '../../components/MainInput' 8 | import ResultsList from '../../components/ResultsList' 9 | import styles from './styles.css' 10 | import { focusableSelector } from 'cerebro-ui' 11 | import * as searchActions from '../../actions/search' 12 | import escapeStringRegexp from 'escape-string-regexp' 13 | 14 | import { debounce, bind } from 'lodash-decorators' 15 | 16 | import trackEvent from 'lib/trackEvent' 17 | import getWindowPosition from 'lib/getWindowPosition' 18 | 19 | import { 20 | WINDOW_WIDTH, 21 | INPUT_HEIGHT, 22 | RESULT_HEIGHT, 23 | MIN_VISIBLE_RESULTS, 24 | } from '../../constants/ui' 25 | 26 | const SHOW_EVENT = { 27 | category: 'Window', 28 | event: 'show' 29 | } 30 | 31 | const SELECT_EVENT = { 32 | category: 'Plugins', 33 | event: 'select' 34 | } 35 | 36 | const trackShowWindow = () => trackEvent(SHOW_EVENT) 37 | const trackSelectItem = (label) => trackEvent({ ...SELECT_EVENT, label }) 38 | 39 | /** 40 | * Wrap click or mousedown event to custom `select-item` event, 41 | * that includes only information about clicked keys (alt, shift, ctrl and meta) 42 | * 43 | * @param {Event} realEvent 44 | * @return {CustomEvent} 45 | */ 46 | const wrapEvent = (realEvent) => { 47 | const event = new CustomEvent('select-item', { cancelable: true }) 48 | event.altKey = realEvent.altKey 49 | event.shiftKey = realEvent.shiftKey 50 | event.ctrlKey = realEvent.ctrlKey 51 | event.metaKey = realEvent.metaKey 52 | return event 53 | } 54 | 55 | /** 56 | * Set focus to first focusable element in preview 57 | */ 58 | const focusPreview = () => { 59 | const previewDom = document.getElementById('preview') 60 | const firstFocusable = previewDom.querySelector(focusableSelector) 61 | if (firstFocusable) { 62 | firstFocusable.focus() 63 | } 64 | } 65 | 66 | /** 67 | * Check if cursor in the end of input 68 | * 69 | * @param {DOMElement} input 70 | */ 71 | const cursorInEndOfInut = ({ selectionStart, selectionEnd, value }) => ( 72 | selectionStart === selectionEnd && selectionStart >= value.length 73 | ) 74 | 75 | /** 76 | * Main search container 77 | * 78 | * TODO: Remove redux 79 | * TODO: Split to more components 80 | */ 81 | class Search extends Component { 82 | constructor(props) { 83 | super(props) 84 | this.electronWindow = remote.getCurrentWindow() 85 | this.state = { 86 | mainInputFocused: false 87 | } 88 | } 89 | 90 | componentWillMount() { 91 | // Listen for window.resize and change default space for results to user's value 92 | window.addEventListener('resize', this.onWindowResize) 93 | // Add some global key handlers 94 | window.addEventListener('keydown', this.onDocumentKeydown) 95 | // Cleanup event listeners on unload 96 | // NOTE: when page refreshed (location.reload) componentWillUnmount is not called 97 | window.addEventListener('beforeunload', this.cleanup) 98 | this.electronWindow.on('show', this.focusMainInput) 99 | this.electronWindow.on('show', this.updateElectronWindow) 100 | this.electronWindow.on('show', trackShowWindow) 101 | } 102 | componentDidMount() { 103 | this.focusMainInput() 104 | this.updateElectronWindow() 105 | } 106 | componentDidUpdate(prevProps) { 107 | const { results } = this.props 108 | if (results.length !== prevProps.results.length) { 109 | // Resize electron window when results count changed 110 | this.updateElectronWindow() 111 | } 112 | } 113 | componentWillUnmount() { 114 | this.cleanup() 115 | } 116 | 117 | /** 118 | * Handle resize window and change count of visible results depends on window size 119 | */ 120 | @bind() 121 | @debounce(100) 122 | onWindowResize() { 123 | if (this.props.results.length <= MIN_VISIBLE_RESULTS) { 124 | return false 125 | } 126 | let visibleResults = Math.floor((window.outerHeight - INPUT_HEIGHT) / RESULT_HEIGHT) 127 | visibleResults = Math.max(MIN_VISIBLE_RESULTS, visibleResults) 128 | if (visibleResults !== this.props.visibleResults) { 129 | this.props.actions.changeVisibleResults(visibleResults) 130 | } 131 | } 132 | 133 | @bind() 134 | onDocumentKeydown(event) { 135 | if (event.keyCode === 27) { 136 | event.preventDefault() 137 | document.getElementById('main-input').focus() 138 | } 139 | } 140 | 141 | /** 142 | * Handle keyboard shortcuts 143 | */ 144 | @bind() 145 | onKeyDown(event) { 146 | const highlighted = this.highlightedResult() 147 | // TODO: go to first result on cmd+up and last result on cmd+down 148 | if (highlighted && highlighted.onKeyDown) { 149 | highlighted.onKeyDown(event) 150 | } 151 | if (event.defaultPrevented) { 152 | return 153 | } 154 | 155 | const keyActions = { 156 | select: () => { 157 | this.selectCurrent(event) 158 | }, 159 | arrowRight: () => { 160 | if (cursorInEndOfInut(event.target)) { 161 | if (this.autocompleteValue()) { 162 | // Autocomplete by arrow right only if autocomple value is shown 163 | this.autocomplete(event) 164 | } else { 165 | focusPreview() 166 | event.preventDefault() 167 | } 168 | } 169 | }, 170 | arrowDown: () => { 171 | this.props.actions.moveCursor(1) 172 | event.preventDefault() 173 | }, 174 | arrowUp: () => { 175 | if (this.props.results.length > 0) { 176 | this.props.actions.moveCursor(-1) 177 | } else if (this.props.prevTerm) { 178 | this.props.actions.updateTerm(this.props.prevTerm) 179 | } 180 | event.preventDefault() 181 | } 182 | } 183 | 184 | 185 | if (event.metaKey || event.ctrlKey) { 186 | if (event.keyCode === 67) { 187 | // Copy to clipboard on cmd+c 188 | const text = this.highlightedResult().clipboard 189 | if (text) { 190 | clipboard.writeText(text) 191 | this.props.actions.reset() 192 | event.preventDefault() 193 | } 194 | return 195 | } 196 | if (event.keyCode >= 49 && event.keyCode <= 57) { 197 | // Select element by number 198 | const number = Math.abs(49 - event.keyCode) 199 | const result = this.props.results[number] 200 | if (result) { 201 | return this.selectItem(result) 202 | } 203 | } 204 | // Lightweight vim-mode: cmd/ctrl + jklo 205 | switch (event.keyCode) { 206 | case 74: 207 | keyActions.arrowDown() 208 | break 209 | case 75: 210 | keyActions.arrowUp() 211 | break 212 | case 76: 213 | keyActions.arrowRight() 214 | break 215 | case 79: 216 | keyActions.select() 217 | break 218 | } 219 | } 220 | switch (event.keyCode) { 221 | case 9: 222 | this.autocomplete(event) 223 | break 224 | case 39: 225 | keyActions.arrowRight() 226 | break 227 | case 40: 228 | keyActions.arrowDown() 229 | break 230 | case 38: 231 | keyActions.arrowUp() 232 | break 233 | case 13: 234 | keyActions.select() 235 | break 236 | case 27: 237 | this.props.actions.reset() 238 | this.electronWindow.hide() 239 | break 240 | } 241 | } 242 | 243 | @bind() 244 | onMainInputFocus() { 245 | this.setState({ mainInputFocused: true }) 246 | } 247 | 248 | @bind() 249 | onMainInputBlur() { 250 | this.setState({ mainInputFocused: false }) 251 | } 252 | 253 | @bind() 254 | cleanup() { 255 | window.removeEventListener('resize', this.onWindowResize) 256 | window.removeEventListener('keydown', this.onDocumentKeydown) 257 | window.removeEventListener('beforeunload', this.cleanup) 258 | this.electronWindow.removeListener('show', this.focusMainInput) 259 | this.electronWindow.removeListener('show', this.updateElectronWindow) 260 | this.electronWindow.removeListener('show', trackShowWindow) 261 | } 262 | 263 | @bind() 264 | focusMainInput() { 265 | this.refs.mainInput.focus() 266 | } 267 | 268 | /** 269 | * Get highlighted result 270 | * @return {Object} 271 | */ 272 | highlightedResult() { 273 | return this.props.results[this.props.selected] 274 | } 275 | 276 | /** 277 | * Select item from results list 278 | * @param {[type]} item [description] 279 | * @return {[type]} [description] 280 | */ 281 | @bind() 282 | selectItem(item, realEvent) { 283 | this.props.actions.reset() 284 | trackSelectItem(item.plugin) 285 | const event = wrapEvent(realEvent) 286 | item.onSelect(event) 287 | if (!event.defaultPrevented) { 288 | this.electronWindow.hide() 289 | } 290 | } 291 | 292 | /** 293 | * Autocomple search term from highlighted result 294 | */ 295 | autocomplete(event) { 296 | const { term } = this.highlightedResult() 297 | if (term && term !== this.props.term) { 298 | this.props.actions.updateTerm(term) 299 | event.preventDefault() 300 | } 301 | } 302 | /** 303 | * Select highlighted element 304 | */ 305 | selectCurrent(event) { 306 | this.selectItem(this.highlightedResult(), event) 307 | } 308 | 309 | /** 310 | * Set resizable and size for main electron window when results count is changed 311 | */ 312 | @bind() 313 | @debounce(16) 314 | updateElectronWindow() { 315 | const { results, visibleResults } = this.props 316 | const { length } = results 317 | const win = this.electronWindow 318 | const [width] = win.getSize() 319 | 320 | // When results list is empty window is not resizable 321 | win.setResizable(length !== 0) 322 | 323 | if (length === 0) { 324 | win.setMinimumSize(WINDOW_WIDTH, INPUT_HEIGHT) 325 | win.setSize(width, INPUT_HEIGHT) 326 | win.setPosition(...getWindowPosition({ width })) 327 | return 328 | } 329 | 330 | const resultHeight = Math.max(Math.min(visibleResults, length), MIN_VISIBLE_RESULTS) 331 | const heightWithResults = resultHeight * RESULT_HEIGHT + INPUT_HEIGHT 332 | const minHeightWithResults = MIN_VISIBLE_RESULTS * RESULT_HEIGHT + INPUT_HEIGHT 333 | win.setMinimumSize(WINDOW_WIDTH, minHeightWithResults) 334 | win.setSize(width, heightWithResults) 335 | win.setPosition(...getWindowPosition({ width, heightWithResults })) 336 | } 337 | 338 | autocompleteValue() { 339 | const selected = this.highlightedResult() 340 | if (selected && selected.term) { 341 | const regexp = new RegExp(`^${escapeStringRegexp(this.props.term)}`, 'i') 342 | if (selected.term.match(regexp)) { 343 | return selected.term.replace(regexp, this.props.term) 344 | } 345 | } 346 | return '' 347 | } 348 | /** 349 | * Render autocomplete suggestion from selected item 350 | * @return {React} 351 | */ 352 | renderAutocomplete() { 353 | const term = this.autocompleteValue() 354 | if (term) { 355 | return
{term}
356 | } 357 | } 358 | render() { 359 | const { mainInputFocused } = this.state 360 | return ( 361 |
362 | {this.renderAutocomplete()} 363 |
364 | 372 |
373 | 381 |
382 | ) 383 | } 384 | } 385 | 386 | Search.propTypes = { 387 | actions: PropTypes.shape({ 388 | reset: PropTypes.func, 389 | moveCursor: PropTypes.func, 390 | updateTerm: PropTypes.func, 391 | changeVisibleResults: PropTypes.func, 392 | selectElement: PropTypes.func, 393 | }), 394 | results: PropTypes.array, 395 | selected: PropTypes.number, 396 | visibleResults: PropTypes.number, 397 | term: PropTypes.string, 398 | prevTerm: PropTypes.string, 399 | } 400 | 401 | function mapStateToProps(state) { 402 | return { 403 | selected: state.search.selected, 404 | results: state.search.resultIds.map(id => state.search.resultsById[id]), 405 | term: state.search.term, 406 | prevTerm: state.search.prevTerm, 407 | visibleResults: state.search.visibleResults, 408 | } 409 | } 410 | 411 | function mapDispatchToProps(dispatch) { 412 | return { 413 | actions: bindActionCreators(searchActions, dispatch), 414 | } 415 | } 416 | 417 | export default connect(mapStateToProps, mapDispatchToProps)(Search) 418 | -------------------------------------------------------------------------------- /app/main/plugins/core/settings/Settings/countries.js: -------------------------------------------------------------------------------- 1 | export default [{ 2 | value: 'AF', 3 | label: 'Afghanistan' 4 | }, { 5 | value: 'AX', 6 | label: 'Åland Islands' 7 | }, { 8 | value: 'AL', 9 | label: 'Albania' 10 | }, { 11 | value: 'DZ', 12 | label: 'Algeria' 13 | }, { 14 | value: 'AS', 15 | label: 'American Samoa' 16 | }, { 17 | value: 'AD', 18 | label: 'Andorra' 19 | }, { 20 | value: 'AO', 21 | label: 'Angola' 22 | }, { 23 | value: 'AI', 24 | label: 'Anguilla' 25 | }, { 26 | value: 'AQ', 27 | label: 'Antarctica' 28 | }, { 29 | value: 'AG', 30 | label: 'Antigua and Barbuda' 31 | }, { 32 | value: 'AR', 33 | label: 'Argentina' 34 | }, { 35 | value: 'AM', 36 | label: 'Armenia' 37 | }, { 38 | value: 'AW', 39 | label: 'Aruba' 40 | }, { 41 | value: 'AU', 42 | label: 'Australia' 43 | }, { 44 | value: 'AT', 45 | label: 'Austria' 46 | }, { 47 | value: 'AZ', 48 | label: 'Azerbaijan' 49 | }, { 50 | value: 'BS', 51 | label: 'The Bahamas' 52 | }, { 53 | value: 'BH', 54 | label: 'Bahrain' 55 | }, { 56 | value: 'BD', 57 | label: 'Bangladesh' 58 | }, { 59 | value: 'BB', 60 | label: 'Barbados' 61 | }, { 62 | value: 'BY', 63 | label: 'Belarus' 64 | }, { 65 | value: 'BE', 66 | label: 'Belgium' 67 | }, { 68 | value: 'BZ', 69 | label: 'Belize' 70 | }, { 71 | value: 'BJ', 72 | label: 'Benin' 73 | }, { 74 | value: 'BM', 75 | label: 'Bermuda' 76 | }, { 77 | value: 'BT', 78 | label: 'Bhutan' 79 | }, { 80 | value: 'BO', 81 | label: 'Bolivia' 82 | }, { 83 | value: 'BQ', 84 | label: 'Bonaire' 85 | }, { 86 | value: 'BA', 87 | label: 'Bosnia and Herzegovina' 88 | }, { 89 | value: 'BW', 90 | label: 'Botswana' 91 | }, { 92 | value: 'BV', 93 | label: 'Bouvet Island' 94 | }, { 95 | value: 'BR', 96 | label: 'Brazil' 97 | }, { 98 | value: 'IO', 99 | label: 'British Indian Ocean Territory' 100 | }, { 101 | value: 'UM', 102 | label: 'United States Minor Outlying Islands' 103 | }, { 104 | value: 'VG', 105 | label: 'Virgin Islands (British)' 106 | }, { 107 | value: 'VI', 108 | label: 'Virgin Islands (U.S.)' 109 | }, { 110 | value: 'BN', 111 | label: 'Brunei' 112 | }, { 113 | value: 'BG', 114 | label: 'Bulgaria' 115 | }, { 116 | value: 'BF', 117 | label: 'Burkina Faso' 118 | }, { 119 | value: 'BI', 120 | label: 'Burundi' 121 | }, { 122 | value: 'KH', 123 | label: 'Cambodia' 124 | }, { 125 | value: 'CM', 126 | label: 'Cameroon' 127 | }, { 128 | value: 'CA', 129 | label: 'Canada' 130 | }, { 131 | value: 'CV', 132 | label: 'Cape Verde' 133 | }, { 134 | value: 'KY', 135 | label: 'Cayman Islands' 136 | }, { 137 | value: 'CF', 138 | label: 'Central African Republic' 139 | }, { 140 | value: 'TD', 141 | label: 'Chad' 142 | }, { 143 | value: 'CL', 144 | label: 'Chile' 145 | }, { 146 | value: 'CN', 147 | label: 'China' 148 | }, { 149 | value: 'CX', 150 | label: 'Christmas Island' 151 | }, { 152 | value: 'CC', 153 | label: 'Cocos (Keeling) Islands' 154 | }, { 155 | value: 'CO', 156 | label: 'Colombia' 157 | }, { 158 | value: 'KM', 159 | label: 'Comoros' 160 | }, { 161 | value: 'CG', 162 | label: 'Republic of the Congo' 163 | }, { 164 | value: 'CD', 165 | label: 'Democratic Republic of the Congo' 166 | }, { 167 | value: 'CK', 168 | label: 'Cook Islands' 169 | }, { 170 | value: 'CR', 171 | label: 'Costa Rica' 172 | }, { 173 | value: 'HR', 174 | label: 'Croatia' 175 | }, { 176 | value: 'CU', 177 | label: 'Cuba' 178 | }, { 179 | value: 'CW', 180 | label: 'Curaçao' 181 | }, { 182 | value: 'CY', 183 | label: 'Cyprus' 184 | }, { 185 | value: 'CZ', 186 | label: 'Czech Republic' 187 | }, { 188 | value: 'DK', 189 | label: 'Denmark' 190 | }, { 191 | value: 'DJ', 192 | label: 'Djibouti' 193 | }, { 194 | value: 'DM', 195 | label: 'Dominica' 196 | }, { 197 | value: 'DO', 198 | label: 'Dominican Republic' 199 | }, { 200 | value: 'EC', 201 | label: 'Ecuador' 202 | }, { 203 | value: 'EG', 204 | label: 'Egypt' 205 | }, { 206 | value: 'SV', 207 | label: 'El Salvador' 208 | }, { 209 | value: 'GQ', 210 | label: 'Equatorial Guinea' 211 | }, { 212 | value: 'ER', 213 | label: 'Eritrea' 214 | }, { 215 | value: 'EE', 216 | label: 'Estonia' 217 | }, { 218 | value: 'ET', 219 | label: 'Ethiopia' 220 | }, { 221 | value: 'FK', 222 | label: 'Falkland Islands' 223 | }, { 224 | value: 'FO', 225 | label: 'Faroe Islands' 226 | }, { 227 | value: 'FJ', 228 | label: 'Fiji' 229 | }, { 230 | value: 'FI', 231 | label: 'Finland' 232 | }, { 233 | value: 'FR', 234 | label: 'France' 235 | }, { 236 | value: 'GF', 237 | label: 'French Guiana' 238 | }, { 239 | value: 'PF', 240 | label: 'French Polynesia' 241 | }, { 242 | value: 'TF', 243 | label: 'French Southern and Antarctic Lands' 244 | }, { 245 | value: 'GA', 246 | label: 'Gabon' 247 | }, { 248 | value: 'GM', 249 | label: 'The Gambia' 250 | }, { 251 | value: 'GE', 252 | label: 'Georgia' 253 | }, { 254 | value: 'DE', 255 | label: 'Germany' 256 | }, { 257 | value: 'GH', 258 | label: 'Ghana' 259 | }, { 260 | value: 'GI', 261 | label: 'Gibraltar' 262 | }, { 263 | value: 'GR', 264 | label: 'Greece' 265 | }, { 266 | value: 'GL', 267 | label: 'Greenland' 268 | }, { 269 | value: 'GD', 270 | label: 'Grenada' 271 | }, { 272 | value: 'GP', 273 | label: 'Guadeloupe' 274 | }, { 275 | value: 'GU', 276 | label: 'Guam' 277 | }, { 278 | value: 'GT', 279 | label: 'Guatemala' 280 | }, { 281 | value: 'GG', 282 | label: 'Guernsey' 283 | }, { 284 | value: 'GW', 285 | label: 'Guinea-Bissau' 286 | }, { 287 | value: 'GY', 288 | label: 'Guyana' 289 | }, { 290 | value: 'HT', 291 | label: 'Haiti' 292 | }, { 293 | value: 'HM', 294 | label: 'Heard Island and McDonald Islands' 295 | }, { 296 | value: 'VA', 297 | label: 'Holy See' 298 | }, { 299 | value: 'HN', 300 | label: 'Honduras' 301 | }, { 302 | value: 'HK', 303 | label: 'Hong Kong' 304 | }, { 305 | value: 'HU', 306 | label: 'Hungary' 307 | }, { 308 | value: 'IS', 309 | label: 'Iceland' 310 | }, { 311 | value: 'IN', 312 | label: 'India' 313 | }, { 314 | value: 'ID', 315 | label: 'Indonesia' 316 | }, { 317 | value: 'CI', 318 | label: 'Ivory Coast' 319 | }, { 320 | value: 'IR', 321 | label: 'Iran' 322 | }, { 323 | value: 'IQ', 324 | label: 'Iraq' 325 | }, { 326 | value: 'IE', 327 | label: 'Republic of Ireland' 328 | }, { 329 | value: 'IM', 330 | label: 'Isle of Man' 331 | }, { 332 | value: 'IL', 333 | label: 'Israel' 334 | }, { 335 | value: 'IT', 336 | label: 'Italy' 337 | }, { 338 | value: 'JM', 339 | label: 'Jamaica' 340 | }, { 341 | value: 'JP', 342 | label: 'Japan' 343 | }, { 344 | value: 'JE', 345 | label: 'Jersey' 346 | }, { 347 | value: 'JO', 348 | label: 'Jordan' 349 | }, { 350 | value: 'KZ', 351 | label: 'Kazakhstan' 352 | }, { 353 | value: 'KE', 354 | label: 'Kenya' 355 | }, { 356 | value: 'KI', 357 | label: 'Kiribati' 358 | }, { 359 | value: 'KW', 360 | label: 'Kuwait' 361 | }, { 362 | value: 'KG', 363 | label: 'Kyrgyzstan' 364 | }, { 365 | value: 'LA', 366 | label: 'Laos' 367 | }, { 368 | value: 'LV', 369 | label: 'Latvia' 370 | }, { 371 | value: 'LB', 372 | label: 'Lebanon' 373 | }, { 374 | value: 'LS', 375 | label: 'Lesotho' 376 | }, { 377 | value: 'LR', 378 | label: 'Liberia' 379 | }, { 380 | value: 'LY', 381 | label: 'Libya' 382 | }, { 383 | value: 'LI', 384 | label: 'Liechtenstein' 385 | }, { 386 | value: 'LT', 387 | label: 'Lithuania' 388 | }, { 389 | value: 'LU', 390 | label: 'Luxembourg' 391 | }, { 392 | value: 'MO', 393 | label: 'Macau' 394 | }, { 395 | value: 'MK', 396 | label: 'Republic of Macedonia' 397 | }, { 398 | value: 'MG', 399 | label: 'Madagascar' 400 | }, { 401 | value: 'MW', 402 | label: 'Malawi' 403 | }, { 404 | value: 'MY', 405 | label: 'Malaysia' 406 | }, { 407 | value: 'MV', 408 | label: 'Maldives' 409 | }, { 410 | value: 'ML', 411 | label: 'Mali' 412 | }, { 413 | value: 'MT', 414 | label: 'Malta' 415 | }, { 416 | value: 'MH', 417 | label: 'Marshall Islands' 418 | }, { 419 | value: 'MQ', 420 | label: 'Martinique' 421 | }, { 422 | value: 'MR', 423 | label: 'Mauritania' 424 | }, { 425 | value: 'MU', 426 | label: 'Mauritius' 427 | }, { 428 | value: 'YT', 429 | label: 'Mayotte' 430 | }, { 431 | value: 'MX', 432 | label: 'Mexico' 433 | }, { 434 | value: 'FM', 435 | label: 'Federated States of Micronesia' 436 | }, { 437 | value: 'MD', 438 | label: 'Moldova' 439 | }, { 440 | value: 'MC', 441 | label: 'Monaco' 442 | }, { 443 | value: 'MN', 444 | label: 'Mongolia' 445 | }, { 446 | value: 'ME', 447 | label: 'Montenegro' 448 | }, { 449 | value: 'MS', 450 | label: 'Montserrat' 451 | }, { 452 | value: 'MA', 453 | label: 'Morocco' 454 | }, { 455 | value: 'MZ', 456 | label: 'Mozambique' 457 | }, { 458 | value: 'MM', 459 | label: 'Myanmar' 460 | }, { 461 | value: 'NA', 462 | label: 'Namibia' 463 | }, { 464 | value: 'NR', 465 | label: 'Nauru' 466 | }, { 467 | value: 'NP', 468 | label: 'Nepal' 469 | }, { 470 | value: 'NL', 471 | label: 'Netherlands' 472 | }, { 473 | value: 'NC', 474 | label: 'New Caledonia' 475 | }, { 476 | value: 'NZ', 477 | label: 'New Zealand' 478 | }, { 479 | value: 'NI', 480 | label: 'Nicaragua' 481 | }, { 482 | value: 'NE', 483 | label: 'Niger' 484 | }, { 485 | value: 'NG', 486 | label: 'Nigeria' 487 | }, { 488 | value: 'NU', 489 | label: 'Niue' 490 | }, { 491 | value: 'NF', 492 | label: 'Norfolk Island' 493 | }, { 494 | value: 'KP', 495 | label: 'North Korea' 496 | }, { 497 | value: 'MP', 498 | label: 'Northern Mariana Islands' 499 | }, { 500 | value: 'NO', 501 | label: 'Norway' 502 | }, { 503 | value: 'OM', 504 | label: 'Oman' 505 | }, { 506 | value: 'PK', 507 | label: 'Pakistan' 508 | }, { 509 | value: 'PW', 510 | label: 'Palau' 511 | }, { 512 | value: 'PS', 513 | label: 'Palestine' 514 | }, { 515 | value: 'PA', 516 | label: 'Panama' 517 | }, { 518 | value: 'PG', 519 | label: 'Papua New Guinea' 520 | }, { 521 | value: 'PY', 522 | label: 'Paraguay' 523 | }, { 524 | value: 'PE', 525 | label: 'Peru' 526 | }, { 527 | value: 'PH', 528 | label: 'Philippines' 529 | }, { 530 | value: 'PN', 531 | label: 'Pitcairn Islands' 532 | }, { 533 | value: 'PL', 534 | label: 'Poland' 535 | }, { 536 | value: 'PT', 537 | label: 'Portugal' 538 | }, { 539 | value: 'PR', 540 | label: 'Puerto Rico' 541 | }, { 542 | value: 'QA', 543 | label: 'Qatar' 544 | }, { 545 | value: 'XK', 546 | label: 'Republic of Kosovo' 547 | }, { 548 | value: 'RE', 549 | label: 'Réunion' 550 | }, { 551 | value: 'RO', 552 | label: 'Romania' 553 | }, { 554 | value: 'RU', 555 | label: 'Russia' 556 | }, { 557 | value: 'RW', 558 | label: 'Rwanda' 559 | }, { 560 | value: 'BL', 561 | label: 'Saint Barthélemy' 562 | }, { 563 | value: 'SH', 564 | label: 'Saint Helena' 565 | }, { 566 | value: 'KN', 567 | label: 'Saint Kitts and Nevis' 568 | }, { 569 | value: 'LC', 570 | label: 'Saint Lucia' 571 | }, { 572 | value: 'MF', 573 | label: 'Saint Martin' 574 | }, { 575 | value: 'PM', 576 | label: 'Saint Pierre and Miquelon' 577 | }, { 578 | value: 'VC', 579 | label: 'Saint Vincent and the Grenadines' 580 | }, { 581 | value: 'WS', 582 | label: 'Samoa' 583 | }, { 584 | value: 'SM', 585 | label: 'San Marino' 586 | }, { 587 | value: 'ST', 588 | label: 'São Tomé and Príncipe' 589 | }, { 590 | value: 'SA', 591 | label: 'Saudi Arabia' 592 | }, { 593 | value: 'SN', 594 | label: 'Senegal' 595 | }, { 596 | value: 'RS', 597 | label: 'Serbia' 598 | }, { 599 | value: 'SC', 600 | label: 'Seychelles' 601 | }, { 602 | value: 'SL', 603 | label: 'Sierra Leone' 604 | }, { 605 | value: 'SG', 606 | label: 'Singapore' 607 | }, { 608 | value: 'SX', 609 | label: 'Sint Maarten' 610 | }, { 611 | value: 'SK', 612 | label: 'Slovakia' 613 | }, { 614 | value: 'SI', 615 | label: 'Slovenia' 616 | }, { 617 | value: 'SB', 618 | label: 'Solomon Islands' 619 | }, { 620 | value: 'SO', 621 | label: 'Somalia' 622 | }, { 623 | value: 'ZA', 624 | label: 'South Africa' 625 | }, { 626 | value: 'GS', 627 | label: 'South Georgia' 628 | }, { 629 | value: 'KR', 630 | label: 'South Korea' 631 | }, { 632 | value: 'SS', 633 | label: 'South Sudan' 634 | }, { 635 | value: 'ES', 636 | label: 'Spain' 637 | }, { 638 | value: 'LK', 639 | label: 'Sri Lanka' 640 | }, { 641 | value: 'SD', 642 | label: 'Sudan' 643 | }, { 644 | value: 'SR', 645 | label: 'Surinae' 646 | }, { 647 | value: 'SJ', 648 | label: 'Svalbard and Jan Mayen' 649 | }, { 650 | value: 'SZ', 651 | label: 'Swaziland' 652 | }, { 653 | value: 'SE', 654 | label: 'Sweden' 655 | }, { 656 | value: 'CH', 657 | label: 'Switzerland' 658 | }, { 659 | value: 'SY', 660 | label: 'Syria' 661 | }, { 662 | value: 'TW', 663 | label: 'Taiwan' 664 | }, { 665 | value: 'TJ', 666 | label: 'Tajikistan' 667 | }, { 668 | value: 'TZ', 669 | label: 'Tanzania' 670 | }, { 671 | value: 'TH', 672 | label: 'Thailand' 673 | }, { 674 | value: 'TL', 675 | label: 'East Timor' 676 | }, { 677 | value: 'TG', 678 | label: 'Togo' 679 | }, { 680 | value: 'TK', 681 | label: 'Tokelau' 682 | }, { 683 | value: 'TO', 684 | label: 'Tonga' 685 | }, { 686 | value: 'TT', 687 | label: 'Trinidad and Tobago' 688 | }, { 689 | value: 'TN', 690 | label: 'Tunisia' 691 | }, { 692 | value: 'TR', 693 | label: 'Turkey' 694 | }, { 695 | value: 'TM', 696 | label: 'Turkmenistan' 697 | }, { 698 | value: 'TC', 699 | label: 'Turks and Caicos Islands' 700 | }, { 701 | value: 'TV', 702 | label: 'Tuvalu' 703 | }, { 704 | value: 'UG', 705 | label: 'Uganda' 706 | }, { 707 | value: 'UA', 708 | label: 'Ukraine' 709 | }, { 710 | value: 'AE', 711 | label: 'United Arab Emirates' 712 | }, { 713 | value: 'GB', 714 | label: 'United Kingdom' 715 | }, { 716 | value: 'US', 717 | label: 'United States' 718 | }, { 719 | value: 'UY', 720 | label: 'Uruguay' 721 | }, { 722 | value: 'UZ', 723 | label: 'Uzbekistan' 724 | }, { 725 | value: 'VU', 726 | label: 'Vanuatu' 727 | }, { 728 | value: 'VE', 729 | label: 'Venezuela' 730 | }, { 731 | value: 'VN', 732 | label: 'Vietnam' 733 | }, { 734 | value: 'WF', 735 | label: 'Wallis and Futuna' 736 | }, { 737 | value: 'EH', 738 | label: 'Western Sahara' 739 | }, { 740 | value: 'YE', 741 | label: 'Yemen' 742 | }, { 743 | value: 'ZM', 744 | label: 'Zambia' 745 | }, { 746 | value: 'ZW', 747 | label: 'Zimbabwe' 748 | }] 749 | --------------------------------------------------------------------------------