Repository: cerebroapp/cerebro
Branch: master
Commit: 1a5c3576584e
Files: 115
Total size: 145.0 KB
Directory structure:
gitextract_k8ttga3x/
├── .commitlintrc.json
├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github/
│ ├── issue_template.md
│ ├── pull_request_template.md
│ └── workflows/
│ ├── build.yml
│ └── pr.yml
├── .gitignore
├── .husky/
│ └── commit-msg
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── __mocks__/
│ ├── @electron/
│ │ └── remote.js
│ ├── electron-store.js
│ ├── electron.js
│ ├── fileMock.js
│ └── plugins.js
├── app/
│ ├── background/
│ │ ├── background.js
│ │ ├── createWindow.js
│ │ └── index.html
│ ├── initAutoUpdater.js
│ ├── lib/
│ │ ├── __tests__/
│ │ │ └── loadThemes.spec.js
│ │ ├── config.js
│ │ ├── initPlugin.js
│ │ ├── initializePlugins.js
│ │ ├── plugins/
│ │ │ ├── index.js
│ │ │ ├── npm.js
│ │ │ └── settings/
│ │ │ ├── __tests__/
│ │ │ │ ├── get.spec.js
│ │ │ │ └── validate.spec.js
│ │ │ ├── get.js
│ │ │ ├── index.js
│ │ │ └── validate.js
│ │ ├── rpc.js
│ │ └── themes.ts
│ ├── main/
│ │ ├── actions/
│ │ │ ├── __tests__/
│ │ │ │ ├── search.spec.js
│ │ │ │ └── statusBar.spec.js
│ │ │ ├── search.js
│ │ │ └── statusBar.ts
│ │ ├── components/
│ │ │ ├── Cerebro/
│ │ │ │ ├── index.js
│ │ │ │ └── styles.module.css
│ │ │ ├── ResultsList/
│ │ │ │ ├── Row/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── styles.module.css
│ │ │ │ ├── index.js
│ │ │ │ └── styles.module.css
│ │ │ ├── SmartIcon/
│ │ │ │ ├── getFileIcon/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── mac.ts
│ │ │ │ │ └── windows.ts
│ │ │ │ └── index.tsx
│ │ │ └── StatusBar/
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ │ ├── constants/
│ │ │ ├── actionTypes.ts
│ │ │ └── ui.ts
│ │ ├── createWindow/
│ │ │ ├── AppTray.js
│ │ │ ├── autoStart.js
│ │ │ ├── buildMenu.js
│ │ │ ├── checkForUpdates.js
│ │ │ ├── handleUrl.js
│ │ │ ├── showWindowWithTerm.ts
│ │ │ └── toggleWindow.ts
│ │ ├── createWindow.js
│ │ ├── css/
│ │ │ ├── global.css
│ │ │ ├── system-font.css
│ │ │ └── themes/
│ │ │ ├── dark.css
│ │ │ └── light.css
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── reducers/
│ │ │ ├── index.js
│ │ │ ├── search.js
│ │ │ └── statusBar.js
│ │ └── store/
│ │ ├── configureStore.js
│ │ └── index.ts
│ ├── main.development.js
│ ├── package.json
│ └── plugins/
│ ├── core/
│ │ ├── autocomplete/
│ │ │ └── index.js
│ │ ├── index.ts
│ │ ├── plugins/
│ │ │ ├── Preview/
│ │ │ │ ├── ActionButton.js
│ │ │ │ ├── FormItem.js
│ │ │ │ ├── Settings.js
│ │ │ │ ├── index.js
│ │ │ │ └── styles.module.css
│ │ │ ├── StatusBar/
│ │ │ │ ├── index.js
│ │ │ │ └── styles.module.css
│ │ │ ├── blacklist.js
│ │ │ ├── format.js
│ │ │ ├── getAvailablePlugins.js
│ │ │ ├── getDebuggingPlugins.js
│ │ │ ├── getInstalledPlugins.js
│ │ │ ├── getReadme.js
│ │ │ ├── index.js
│ │ │ ├── initializeAsync.js
│ │ │ └── loadPlugins.js
│ │ ├── quit/
│ │ │ └── index.js
│ │ ├── reload/
│ │ │ └── index.js
│ │ ├── settings/
│ │ │ ├── Settings/
│ │ │ │ ├── Hotkey.js
│ │ │ │ ├── countries.js
│ │ │ │ ├── index.js
│ │ │ │ └── styles.module.css
│ │ │ └── index.js
│ │ └── version/
│ │ └── index.js
│ ├── externalPlugins.js
│ └── index.ts
├── babel.config.js
├── build/
│ ├── icon.icns
│ └── installer.nsh
├── electron-builder.json
├── jest.config.js
├── package.json
├── postcss.config.js
├── server.js
├── tsconfig.json
├── webpack.config.base.js
├── webpack.config.development.js
├── webpack.config.electron.js
└── webpack.config.production.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .commitlintrc.json
================================================
{
"extends": [
"@commitlint/config-conventional"
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{json,js,jsx,ts,tsx,html,css}]
indent_style = space
indent_size = 2
[.eslintrc]
indent_style = space
indent_size = 2
[.travis.yml]
indent_style = space
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintrc.json
================================================
{
"parser": "@babel/eslint-parser",
"extends": "airbnb",
"env": {
"browser": true,
"node": true,
"jest": true
},
"rules": {
"consistent-return": 0,
"comma-dangle": 0,
"no-use-before-define": 0,
"no-console": 0,
"semi": ["error", "never"],
"no-confusing-arrow": ["off"],
"no-useless-escape": 0,
"no-mixed-operators": "off",
"no-continue": "off",
"no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }],
"import/no-extraneous-dependencies": "off",
"import/imports-first": "off",
"import/extensions": "off",
"react/jsx-no-bind": 0,
"react/prefer-stateless-function": 0,
"react/no-string-refs": "off",
"react/forbid-prop-types": "off",
"react/no-unused-prop-types": "off",
"react/no-danger": "off",
"react/require-default-props": "off",
"react/jsx-filename-extension": "off",
"react/no-unescaped-entities": "off",
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
"prefer-spread": "off",
"class-methods-use-this": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/label-has-for": "off",
"linebreak-style": 0
},
"plugins": [
"jsx-a11y",
"import",
"react",
"jest"
],
"settings": {
"import/core-modules": "electron",
"import/resolver": {
"node": {
"paths": ["app"],
"extensions": [".js", ".jsx", ".ts", ".tsx"]
},
"typescript": {
"project": "./tsconfig.json"
}
}
}
}
================================================
FILE: .gitattributes
================================================
* text=auto
================================================
FILE: .github/issue_template.md
================================================
<!--
Hi there! Thank you for discovering and submitting an issue.
Before you submit this; let's make sure of a few things.
Please make sure the following boxes are ticked if they are correct.
If not, please try and fulfil these first.
-->
<!-- Checked checkbox should look like this: [x] -->
- [ ] I am on the [latest](https://github.com/cerebroapp/cerebro/releases/latest) Cerebro.app version
- [ ] I have searched the [issues](https://github.com/cerebroapp/cerebro/issues) of this repo and believe that this is not a duplicate
<!--
Once those are done, if you're able to fill in the following list with your information,
it'd be very helpful to whoever handles the issue.
**Hint**: To open devtools use next:
* Preferences -> Turn on "Developer mode"
* Preferences -> Turn on "Show in menu bar"
* After that you can select tray menu -> Development -> Dev. tools (main)
-->
- **OS version and name**: <!-- Replace with version + name -->
- **Cerebro.app version**: <!-- Replace with version -->
- **Relevant information from devtools** _(See above how to open it)_: <!-- Replace with info if applicable, or N/A -->
## Issue
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
================================================
FILE: .github/pull_request_template.md
================================================
================================================
FILE: .github/workflows/build.yml
================================================
name: Build/release
on:
push:
tags:
- '*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16
- run: yarn
- run: yarn test --detectOpenHandles --forceExit
release:
needs: test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
with:
node-version: 16
- name: Build & Release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: true
================================================
FILE: .github/workflows/pr.yml
================================================
name: Run Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16
- run: yarn
- run: yarn test --detectOpenHandles --forceExit
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Tern-js work files
.tern-port
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# Folder view configuration files
.DS_Store
Desktop.ini
# Thumbnail cache files
._*
Thumbs.db
# App packaged
dist
release
/app/main.js
/app/main.js.map
.tmp/
# IDEs
.idea
*.sublime-project
*.sublime-workspace
.env
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
================================================
FILE: .vscode/settings.json
================================================
{
"search.exclude": {
".git": true,
".eslintcache": true,
"app/dist": true,
"app/main.prod.js": true,
"app/main.prod.js.map": true,
"dll": true,
"release": true,
"node_modules": true,
"npm-debug.log.*": true,
"test/**/__snapshots__": true,
"yarn.lock": true,
".tmp": true
}
}
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) Alexandr Subbotin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Cerebro
> Cerebro is an open-source launcher to improve your productivity and efficiency
<img src="./build/icons/128x128.png" align="right"/>
## Usage
You can download the latest version on the [releases](https://github.com/cerebroapp/cerebro/releases) page.
- If there isn't an installer for your OS, check [build instructions](#build-executable-from-source).
- If you are a linux user see [how to install the executable](#install-executable-on-linux)
After the installation, use the default shortcut, `ctrl+space`, to show the app window. You can customize this shortcut by clicking on the icon in the menu bar, and then selecting "Preferences...".

### Plugins
### Core plugins
- Search the web with your favourite search engine
- Search & launch application, i.e. `spotify`
- Navigate the file system with file previews (i.e. `~/Dropbox/passport.pdf`)
- Calculator
- Smart converter. `15$`, `150 рублей в евро`, `100 eur in gbp`;
### Install plugins
You can manage and install more plugins by typing `plugins <plugin-name>` in the Cerebro search bar.
Discover plugins and more at [Cerebro's Awesome List](https://github.com/lubien/awesome-cerebro).
> If you're interested in creating your own plugin, check the [plugins documentation](https://github.com/cerebroapp/create-cerebro-plugin).
## Shortcuts
Cerebro provides several shortcuts to improve your productivity:
- `ctrl+c`: copy the result from a plugin to the clipboard, if the plugin does not provida a result, the term you introduced will be copied
- `ctrl+1...9`: select directly a result from the list
- `ctrl+[hjkl]`: navigate through the results using vim-like keys (Also `ctrl+o` to select the result)
### Change Theme
Use the shortcut `ctrl+space` to open the app window, and type `Cerebro Settings`. There you will be able to change the Theme.
> Currently Light and Dark Themes are supported out of the box

### Config file path
You can find the config file in the following path depending on your OS:
*Windows*: `%APPDATA%/Cerebro/config.json`
*Linux*: `$XDG_CONFIG_HOME/Cerebro/config.json` or `~/.config/Cerebro/config.json`
*macOS*: `~/Library/Application Support/Cerebro/config.json`
> ⚠️ A bad configuration file can break Cerebro. If you're not sure what you're doing, don't edit the config file directly.
## Build executable from source
If you'd like to install a version of Cerebro, but the executable hasn't been released, you can follow these instructions to build it from source:
1. Clone the repository
2. Install dependencies with [yarn](https://yarnpkg.com/getting-started/install):
```bash
yarn --force
```
3. Build the package:
```bash
yarn package
```
> Note: in CI we use `yarn build` as there is an action to package and publish the executables
## Install executable on Linux
If you're a linux user, you might need to grant execution permissions to the executable. To do so, open the terminal and run the following command:
```bash
sudo chmod +x <path to the executable>
```
Then, you can install the executable by running the following command:
- If you're using the AppImage executable:
```bash
./<path to the executable>
```
- If you're using the deb executable:
```bash
dpkg -i <path to the executable>
```
> On some computers you might need run these commands with elevated privileges (sudo). `sudo ./<path to the executable>` or `sudo dpkg -i <path to the executable>`
## Contributing
CerebroApp is an open source project and we welcome contributions from the community.
In this document you will find information about how Cerebro works and how to contribute to the project.
> ⚠️ NOTE: This document is for Cerebro developers. If you are looking for how to develop a plugin please check [plugin developers documentation](https://github.com/cerebroapp/create-cerebro-plugin).
### General architecture
Cerebro is based on [Electron](https://electronjs.org/) and [React](https://reactjs.org/).
A basic Electron app is composed of a *main process* and a *renderer process*. The main process is responsible for the app lifecycle, the renderer process is responsible for the UI.
In our case we use:
- [`app/main.development.js`](/app/main.development.js) as the main process
- [`app/main/main.js`](/app/main/main.js) as the main renderer process
- [`app/background/background.js`](/app/background/background.js) as a secondary renderer process
All this files are bundled and transpiled with [Webpack](https://webpack.js.org/) and [Babel](https://babeljs.io/).
The build process is managed by [electron-builder](https://www.electron.build/).
### Two renderer processes
This two-renderer process architecture is used to keep the main renderer process (Cerebro) responsive and to avoid blocking the UI when executing long tasks.
When we need to execute a long task we send a message to the background process, which executes the task asynchronously and sends a message back to the main renderer when the task is completed.
This is the way we implement the plugins system. Their initializeAsync method is executed in the background process.
### Prerequisites
- [Node.js](https://nodejs.org/en/) (>= 16)
- [yarn](https://classic.yarnpkg.com/en/)
### Install Cerebro
First, clone the repo via git:
```bash
git clone https://github.com/cerebroapp/cerebro.git cerebro
```
Open the project
```bash
cd cerebro
```
And then install dependencies:
```bash
yarn
```
### Run in development mode
```bash
yarn run dev
```
> Note: requires a node version >=16.x
### Resolve common issues
1. `AssertionError: Current node version is not supported for development` on npm postinstall.
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.
2. `Uncaught Error: Module version mismatch. Exepcted 50, got ...`
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 `yarn --force`
### Conventional Commit Format
The project is using conventional commit specification to keep track of changes. This helps us with the realeases and enforces a consistent style.
You can commit as usually following this style or use the following commands that will help you to commit with the right style:
- `yarn cz`
- `yarn commit`
### Publish a release
CerebroApp is using GH actions to build the app and publish it to a release. To publish a new release follow the steps below:
1. Update the version on both `package.json` and `app/package.json` files.
2. Create a release with from GH and publish it. 🚧 The release **tag** MUST contain the `v` prefix (❌ `0.1.2` → ✅`v0.1.2`).
3. Complete the name with a name and a description of the release.
4. The GH action is triggered and the release is updated when executables are built.
## License
MIT © [Cerebro App](https://github.com/cerebroapp/cerebro/blob/master/LICENSE)
================================================
FILE: __mocks__/@electron/remote.js
================================================
module.exports = {
app: {
getPath: () => '',
getLocale: () => '',
},
}
================================================
FILE: __mocks__/electron-store.js
================================================
class Store {
get() {
return {}
}
set() {}
}
module.exports = Store
================================================
FILE: __mocks__/electron.js
================================================
module.exports = {
app: {
getPath: jest.fn(),
getLocale: jest.fn(),
},
ipcRenderer: {
on: jest.fn(),
}
}
================================================
FILE: __mocks__/fileMock.js
================================================
module.exports = ''
================================================
FILE: __mocks__/plugins.js
================================================
module.exports = {
'test-plugin': {
fn: () => {}
}
}
================================================
FILE: app/background/background.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import plugins from 'plugins'
import { on, send } from 'lib/rpc'
import { settings as pluginSettings, modulesDirectory } from 'lib/plugins'
global.React = React
global.ReactDOM = ReactDOM
on('initializePluginAsync', ({ name }) => {
const { allPlugins } = plugins
console.group(`Initialize async plugin ${name}`)
try {
const plugin = allPlugins[name] || window.require(`${modulesDirectory}/${name}`)
const { initializeAsync } = plugin
if (!initializeAsync) {
console.log('no `initializeAsync` function, skipped')
return
}
console.log('running `initializeAsync`')
initializeAsync((data) => {
console.log('Done! Sending data back to main window')
// Send message back to main window with initialization result
send('plugin.message', { name, data })
}, pluginSettings.getUserSettings(plugin, name))
} catch (err) { console.log('Failed', err) }
console.groupEnd()
})
================================================
FILE: app/background/createWindow.js
================================================
import { BrowserWindow } from 'electron'
export default ({ src }) => {
const backgroundWindow = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: false,
enableRemoteModule: true,
contextIsolation: false
},
})
backgroundWindow.loadURL(src)
return backgroundWindow
}
================================================
FILE: app/background/index.html
================================================
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="script-src * 'unsafe-inline' 'unsafe-eval';" />
<script>
global.isBackground = true;
(function () {
const script = document.createElement('script');
script.async = true;
script.src = (process.env.NODE_ENV)
? 'http://localhost:3000/dist/background.bundle.js'
: '../dist/background.bundle.js';
document.write(script.outerHTML);
}());
</script>
</head>
<body>
</body>
</html>
================================================
FILE: app/initAutoUpdater.js
================================================
import { autoUpdater } from 'electron-updater'
const event = 'update-downloaded'
const TEN_SECONDS = 10 * 1000
const ONE_HOUR = 60 * 60 * 1000
export default (w) => {
if (process.env.NODE_ENV === 'development' || process.platform === 'linux') {
return
}
autoUpdater.on(event, (payload) => {
w.webContents.send('message', {
message: event,
payload
})
})
setTimeout(() => {
autoUpdater.checkForUpdates()
}, TEN_SECONDS)
setInterval(() => {
autoUpdater.checkForUpdates()
}, ONE_HOUR)
}
================================================
FILE: app/lib/__tests__/loadThemes.spec.js
================================================
import themes from '../themes'
const productionThemes = [
{
value: '../dist/main/css/themes/light.css',
label: 'Light'
},
{
value: '../dist/main/css/themes/dark.css',
label: 'Dark'
}
]
test('returns themes for production', () => {
expect(themes).toEqual(productionThemes)
})
================================================
FILE: app/lib/config.js
================================================
import { ipcRenderer } from 'electron'
import Store from 'electron-store'
import themes from './themes'
const schema = {
locale: { type: 'string', default: 'en-US' },
lang: { type: 'string', default: 'en' },
country: { type: 'string', default: 'US' },
theme: { type: 'string', default: themes[0].value },
hotkey: { type: 'string', default: 'Control+Space' },
showInTray: { type: 'boolean', default: true },
firstStart: { type: 'boolean', default: true },
developerMode: { type: 'boolean', default: false },
cleanOnHide: { type: 'boolean', default: true },
selectOnShow: { type: 'boolean', default: false },
hideOnBlur: { type: 'boolean', default: true },
plugins: { type: 'object', default: {} },
isMigratedPlugins: { type: 'boolean', default: false },
openAtLogin: { type: 'boolean', default: true },
winPosition: { type: 'array', default: [] },
searchBarPlaceholder: { type: 'string', default: 'Cerebro Search' },
}
const store = new Store({
schema,
migrations: {
'>=0.9.0': (oldStore) => {
oldStore.delete('positions')
},
'>=0.10.0': (oldStore) => {
oldStore.delete('crashreportingEnabled')
}
}
})
/**
* Get a value from global configuration
* @param {String} key
* @return {Any}
*/
const get = (key) => store.get(key)
/**
* Write a value to global config. It immedately rewrites global config
* and notifies all listeners about changes
*
* @param {String} key
* @param {Any} value
*/
const set = (key, value) => {
store.set(key, value)
if (ipcRenderer) {
console.log('notify main process', key, value)
// Notify main process about settings changes
ipcRenderer.send('updateSettings', key, value)
}
}
export default { get, set }
================================================
FILE: app/lib/initPlugin.js
================================================
import { send } from 'lib/rpc'
import { settings as pluginSettings } from 'lib/plugins'
/**
* Initialices plugin sync and/or async by calling the `initialize` and `initializeAsync` functions
* @param {Object} plugin A plugin object
* @param {string} name The name entry in the plugin package.json
*/
const initPlugin = (plugin, name) => {
const { initialize, initializeAsync } = plugin
// Foreground plugin initialization
if (initialize) {
console.log('Initialize sync plugin', name)
try {
initialize(pluginSettings.getUserSettings(plugin, name))
} catch (e) {
console.error(`Failed to initialize plugin: ${name}`, e)
}
}
// Background plugin initialization
if (initializeAsync) {
console.log('Initialize async plugin', name)
send('initializePluginAsync', { name })
}
}
export default initPlugin
================================================
FILE: app/lib/initializePlugins.js
================================================
import { on } from 'lib/rpc'
import plugins from 'plugins'
import initPlugin from './initPlugin'
/**
* Initialize all plugins and start listening for replies from plugin async initializers
*/
const initializePlugins = () => {
const { allPlugins } = plugins
Object.keys(allPlugins).forEach((name) => initPlugin(allPlugins[name], name))
// Start listening for replies from plugin async initializers
on('plugin.message', ({ name, data }) => {
const plugin = allPlugins[name]
if (plugin && plugin.onMessage) plugin.onMessage(data)
})
}
export default initializePlugins
================================================
FILE: app/lib/plugins/index.js
================================================
import path from 'path'
import fs from 'fs'
import npm from './npm'
const ensureFile = (src, content = '') => {
if (!fs.existsSync(src)) {
fs.writeFileSync(src, content)
}
}
const ensureDir = (src) => {
if (!fs.existsSync(src)) {
fs.mkdirSync(src)
}
}
const EMPTY_PACKAGE_JSON = JSON.stringify({
name: 'cerebro-plugins',
dependencies: {}
}, null, 2)
export const pluginsPath = path.join(process.env.CEREBRO_DATA_PATH, 'plugins')
export const modulesDirectory = path.join(pluginsPath, 'node_modules')
export const packageJsonPath = path.join(pluginsPath, 'package.json')
export const ensureFiles = () => {
ensureDir(pluginsPath)
ensureDir(modulesDirectory)
ensureFile(packageJsonPath, EMPTY_PACKAGE_JSON)
}
export const client = npm(pluginsPath)
export { default as settings } from './settings'
================================================
FILE: app/lib/plugins/npm.js
================================================
import fs from 'fs'
import os from 'os'
import path from 'path'
import tar from 'tar-fs'
import zlib from 'zlib'
import https from 'https'
import { move, remove } from 'fs-extra'
/**
* Base url of npm API
*
* @type {String}
*/
const API_BASE = 'http://registry.npmjs.org/'
/**
* Format name of file from package archive.
* Just remove `./package`prefix from name
*
* @param {Object} header
* @return {Object}
*/
const formatPackageFile = (header) => ({
...header,
name: header.name.replace(/^package\//, '')
})
const installPackage = async (tarPath, destination, middleware) => {
console.log(`Extract ${tarPath} to ${destination}`)
const packageName = path.parse(destination).name
const tempPath = path.join(os.tmpdir(), packageName)
console.log(`Download and extract to temp path: ${tempPath}`)
await new Promise((resolve, reject) => {
https.get(tarPath, (stream) => {
const result = stream
.pipe(zlib.Unzip())
.pipe(tar.extract(tempPath, {
map: formatPackageFile
}))
result.on('error', reject)
result.on('finish', () => {
middleware().then(resolve)
})
})
})
console.log(`Move ${tempPath} to ${destination}`)
// Move temp folder to real location
await move(tempPath, destination, { overwrite: true })
}
/**
* Lightweight npm client.
* It only can install/uninstall package, without resolving dependencies
*
* @param {String} path Path to npm package directory
* @return {Object}
*/
export default (dir) => {
const packageJson = path.join(dir, 'package.json')
const setConfig = (config) => (
fs.writeFileSync(packageJson, JSON.stringify(config, null, 2))
)
const getConfig = () => JSON.parse(fs.readFileSync(packageJson))
return {
/**
* Install npm package
* @param {String} name Name of package in npm registry
*
* @param {Object} options
* version {String} Version of npm package. Default is latest version
* middleware {Function<Promise>}
* Function that returns promise. Called when package's archive is extracted
* to temp folder, but before moving to real location
* @return {Promise}
*/
async install(name, options = {}) {
let versionToInstall
const version = options.version || null
const middleware = options.middleware || (() => Promise.resolve())
console.group('[npm] Install package', name)
try {
const resJson = await fetch(`${API_BASE}${name}`).then((response) => response.json())
versionToInstall = version || resJson['dist-tags'].latest
console.log('Version:', versionToInstall)
await installPackage(
resJson.versions[versionToInstall].dist.tarball,
path.join(dir, 'node_modules', name),
middleware
)
const json = getConfig()
json.dependencies[name] = versionToInstall
console.log('Add package to dependencies')
setConfig(json)
console.log('Finished installing', name)
console.groupEnd()
} catch (err) {
console.log('Error in package installation')
console.log(err)
console.groupEnd()
}
},
update(name) {
// Plugin update is downloading `.tar` and unarchiving it to temp folder
// Only if this part was succeeded, current version of plugin is uninstalled
// and temp folder moved to real plugin location
const middleware = () => this.uninstall(name)
return this.install(name, { middleware })
},
/**
* Uninstall npm package
*
* @param {String} name
* @return {Promise}
*/
async uninstall(name) {
const modulePath = path.join(dir, 'node_modules', name)
console.group('[npm] Uninstall package', name)
console.log('Remove package directory ', modulePath)
try {
await remove(modulePath)
const json = getConfig()
console.log('Update package.json')
delete json.dependencies?.[name]
console.log('Rewrite package.json')
setConfig(json)
console.groupEnd()
return true
} catch (err) {
console.log('Error in package uninstallation')
console.log(err)
console.groupEnd()
}
}
}
}
================================================
FILE: app/lib/plugins/settings/__tests__/get.spec.js
================================================
import getUserSettings from '../get'
const plugin = {
settings: {
test_setting1: {
type: 'string',
defaultValue: 'test',
},
test_setting2: {
type: 'number',
defaultValue: 1,
},
}
}
describe('Test getUserSettings', () => {
it('returns valid settings object', () => {
expect(getUserSettings(plugin, 'test-plugin'))
.toEqual({ test_setting1: 'test', test_setting2: 1 })
})
})
================================================
FILE: app/lib/plugins/settings/__tests__/validate.spec.js
================================================
import validate from '../validate'
const validSettings = {
option1: {
description: 'Just a test description',
type: 'option',
options: ['option_1', 'option_2'],
},
option2: {
description: 'Just a test description',
type: 'number',
defaultValue: 0
},
option3: {
description: 'Just a test description',
type: 'number',
defaultValue: 0
},
option4: {
description: 'Just a test description',
type: 'bool'
},
option5: {
description: 'Just a test description',
type: 'string',
defaultValue: 'test'
}
}
const invalidSettingsNoOptionsProvided = {
option1: {
description: 'Just a test description',
type: 'option',
options: [],
}
}
const invalidSettingsInvalidType = {
option1: {
description: 'Just a test description',
type: 'test'
}
}
describe('Validate settings function', () => {
it('returns true when plugin has no settings field', () => {
const plugin = {
fn: () => {}
}
expect(validate(plugin)).toEqual(true)
})
it('returns true when plugin has empty settings field', () => {
const plugin = {
fn: () => {},
settings: {}
}
expect(validate(plugin)).toEqual(true)
})
it('returns true when plugin has valid settings', () => {
const plugin = {
fn: () => {},
settings: validSettings
}
expect(validate(plugin)).toEqual(true)
})
it('returns false when option type is options and no options provided', () => {
const plugin = {
fn: () => {},
settings: invalidSettingsNoOptionsProvided
}
expect(validate(plugin)).toEqual(false)
})
it('returns false when option type is incorrect', () => {
const plugin = {
fn: () => {},
settings: invalidSettingsInvalidType
}
expect(validate(plugin)).toEqual(false)
})
})
================================================
FILE: app/lib/plugins/settings/get.js
================================================
import config from 'lib/config'
/**
* Returns the settings established by the user and previously saved in the config file
* @param {string} pluginName The name entry of the plugin package.json
* @returns An object with keys and values of the **stored** plugin settings
*/
const getExistingSettings = (pluginName) => config.get('plugins')[pluginName] || {}
/**
* Returns the sum of the default settings and the user settings
* We use packageJsonName to avoid conflicts with plugins that export
* a different name from the bundle. Two plugins can export the same name
* but can't have the same package.json name
* @param {Object} plugin
* @param {string} packageJsonName
* @returns An object with keys and values of the plugin settings
*/
const getUserSettings = (plugin, packageJsonName) => {
const userSettings = {}
const existingSettings = getExistingSettings(packageJsonName)
const { settings: pluginSettings } = plugin
if (pluginSettings) {
// Provide default values if nothing is set by user
Object.keys(pluginSettings).forEach((key) => {
userSettings[key] = existingSettings[key] || pluginSettings[key].defaultValue
})
}
return userSettings
}
export default getUserSettings
================================================
FILE: app/lib/plugins/settings/index.js
================================================
import getUserSettings from './get'
import validate from './validate'
export default { getUserSettings, validate }
================================================
FILE: app/lib/plugins/settings/validate.js
================================================
import { every } from 'lodash/fp'
const VALID_TYPES = new Set([
'string',
'number',
'bool',
'option',
])
const validSetting = ({ type, options }) => {
// General validation of settings
if (!type || !VALID_TYPES.has(type)) return false
// Type-specific validations
if (type === 'option') return Array.isArray(options) && options.length
return true
}
export default ({ settings }) => {
if (!settings) return true
return every(validSetting)(settings)
}
================================================
FILE: app/lib/rpc.js
================================================
import { ipcRenderer } from 'electron'
import EventEmitter from 'events'
const emitter = new EventEmitter()
/**
* Channel name that is managed by main process.
* @type {String}
*/
const CHANNEL = 'message'
// Start listening for rpc channel
ipcRenderer.on(CHANNEL, (_, { message, payload }) => {
console.log(`[rpc] emit ${message}`)
emitter.emit(message, payload)
})
/**
* Send message to rpc-channel
* @param {String} message
* @param {any} payload
*/
export const send = (message, payload) => {
console.log(`[rpc] send ${message}`)
ipcRenderer.send(CHANNEL, { message, payload })
}
export const on = emitter.on.bind(emitter)
export const off = emitter.removeListener.bind(emitter)
export const once = emitter.once.bind(emitter)
================================================
FILE: app/lib/themes.ts
================================================
const prefix = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/' : '../'
type Theme = { value: string, label: string}
const themes: Array<Theme> = [
{
value: `${prefix}dist/main/css/themes/light.css`,
label: 'Light'
},
{
value: `${prefix}dist/main/css/themes/dark.css`,
label: 'Dark'
}
]
export default themes
================================================
FILE: app/main/actions/__tests__/search.spec.js
================================================
/**
* @jest-environment jsdom
*/
import {
MOVE_CURSOR,
SELECT_ELEMENT,
UPDATE_RESULT,
HIDE_RESULT,
RESET,
} from 'main/constants/actionTypes'
import * as actions from '../search'
describe('reset', () => {
it('returns valid action', () => {
expect(actions.reset()).toEqual({
type: RESET,
})
})
})
describe('moveCursor', () => {
it('returns valid action for +1', () => {
expect(actions.moveCursor(1)).toEqual({
type: MOVE_CURSOR,
payload: 1
})
})
it('returns valid action for -1', () => {
expect(actions.moveCursor(-1)).toEqual({
type: MOVE_CURSOR,
payload: -1
})
})
})
describe('selectElement', () => {
it('returns valid action', () => {
expect(actions.selectElement(15)).toEqual({
type: SELECT_ELEMENT,
payload: 15
})
})
})
describe('updateTerm', () => {
describe('for empty term', () => {
it('returns reset action', () => {
expect(actions.updateTerm('')).toEqual({
type: RESET,
})
})
})
})
describe('updateElement', () => {
it('returns valid action', () => {
const id = 1
const result = { title: 'updated' }
expect(actions.updateElement(id, result)).toEqual({
type: UPDATE_RESULT,
payload: { id, result }
})
})
})
describe('hideElement', () => {
it('returns valid action', () => {
const id = 1
expect(actions.hideElement(id)).toEqual({
type: HIDE_RESULT,
payload: { id }
})
})
})
================================================
FILE: app/main/actions/__tests__/statusBar.spec.js
================================================
/**
* @jest-environment jsdom
*/
import {
SET_STATUS_BAR_TEXT
} from 'main/constants/actionTypes'
import * as actions from '../statusBar'
describe('reset', () => {
it('returns valid action', () => {
expect(actions.reset()).toEqual({
type: SET_STATUS_BAR_TEXT,
payload: null
})
})
})
describe('setValue', () => {
it('returns valid action when value passed', () => {
expect(actions.setValue('test value')).toEqual({
type: SET_STATUS_BAR_TEXT,
payload: 'test value'
})
})
})
================================================
FILE: app/main/actions/search.js
================================================
import plugins from 'plugins'
import config from 'lib/config'
import { shell, clipboard } from 'electron'
import { settings as pluginSettings } from 'lib/plugins'
import {
UPDATE_TERM,
MOVE_CURSOR,
SELECT_ELEMENT,
SHOW_RESULT,
HIDE_RESULT,
UPDATE_RESULT,
RESET,
CHANGE_VISIBLE_RESULTS,
} from 'main/constants/actionTypes'
import store from '../store'
const remote = process.type === 'browser'
? undefined
: require('@electron/remote')
/**
* Default scope object would be first argument for plugins
*
* @type {Object}
*/
const DEFAULT_SCOPE = {
config,
actions: {
open: (q) => shell.openExternal(q),
reveal: (q) => shell.showItemInFolder(q),
copyToClipboard: (q) => clipboard.writeText(q),
replaceTerm: (term) => store.dispatch(updateTerm(term)),
hideWindow: () => remote.getCurrentWindow().hide()
}
}
/**
* Pass search term to all plugins and handle their results
* @param {String} term Search term
* @param {Function} display Callback function that receives used search term and found results
*/
const eachPlugin = (term, display) => {
const { allPlugins } = plugins
// TODO: order results by frequency?
Object.keys(allPlugins).forEach((name) => {
const plugin = allPlugins[name]
try {
plugin.fn({
...DEFAULT_SCOPE,
term,
hide: (id) => store.dispatch(hideElement(`${name}-${id}`)),
update: (id, result) => store.dispatch(updateElement(`${name}-${id}`, result)),
display: (payload) => display(name, payload),
settings: pluginSettings.getUserSettings(plugin, name)
})
} catch (error) {
// Do not fail on plugin errors, just log them to console
console.log('Error running plugin', name, error)
}
})
}
/**
* Handle results found by plugin
*
* @param {String} term Search term that was used for found results
* @param {Array | Object} result Found results (or result)
* @return {Object} redux action
*/
function onResultFound(term, result) {
return {
type: SHOW_RESULT,
payload: {
result,
term,
}
}
}
/**
* Action that clears everthing in search box
*
* @return {Object} redux action
*/
export function reset() {
return { type: RESET }
}
/**
* Action that updates search term
*
* @param {String} term
* @return {Object} redux action
*/
export function updateTerm(term) {
if (term === '') return reset()
return (dispatch) => {
dispatch({
type: UPDATE_TERM,
payload: term,
})
eachPlugin(term, (plugin, payload) => {
let result = Array.isArray(payload) ? payload : [payload]
result = result.map((x) => ({
...x,
plugin,
// Scope result ids with plugin name and use title if id is empty
id: `${plugin}-${x.id || x.title}`
}))
if (result.length === 0) {
// Do not dispatch for empty results
return
}
dispatch(onResultFound(term, result))
})
}
}
/**
* Action to move highlighted cursor to next or prev element
* @param {1 | -1} diff
* @return {Object} redux action
*/
export function moveCursor(diff) {
return {
type: MOVE_CURSOR,
payload: diff
}
}
/**
* Action to change highlighted element
* @param {number} index Index of new highlighted element
* @return {Object} redux action
*/
export function selectElement(index) {
return {
type: SELECT_ELEMENT,
payload: index
}
}
/**
* Action to remove element from results list by id
* @param {String} id
* @return {Object} redux action
*/
export function hideElement(id) {
return {
type: HIDE_RESULT,
payload: { id }
}
}
/**
* Action to update displayed element with new result
* @param {String} id
* @return {Object} redux action
*/
export function updateElement(id, result) {
return {
type: UPDATE_RESULT,
payload: { id, result }
}
}
/**
* Change count of visible results (without scroll) in list
*/
export function changeVisibleResults(count) {
return {
type: CHANGE_VISIBLE_RESULTS,
payload: count,
}
}
================================================
FILE: app/main/actions/statusBar.ts
================================================
import {
SET_STATUS_BAR_TEXT
} from '../constants/actionTypes'
export function reset(): { type: string, payload: null } {
return {
type: SET_STATUS_BAR_TEXT,
payload: null
}
}
export function setValue(text: string): { type: string, payload: string } {
return {
type: SET_STATUS_BAR_TEXT,
payload: text
}
}
================================================
FILE: app/main/components/Cerebro/index.js
================================================
/* eslint default-case: 0 */
import React, {
useEffect, useRef, useState
} from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { clipboard } from 'electron'
import { focusableSelector } from '@cerebroapp/cerebro-ui'
import escapeStringRegexp from 'escape-string-regexp'
import {
WINDOW_WIDTH,
INPUT_HEIGHT,
RESULT_HEIGHT,
MIN_VISIBLE_RESULTS,
} from 'main/constants/ui'
import * as searchActions from 'main/actions/search'
import config from 'lib/config'
import ResultsList from '../ResultsList'
import StatusBar from '../StatusBar'
import styles from './styles.module.css'
const remote = require('@electron/remote')
/**
* Wrap click or mousedown event to custom `select-item` event,
* that includes only information about clicked keys (alt, shift, ctrl and meta)
*
* @param {Event} realEvent
* @return {CustomEvent}
*/
const wrapEvent = (realEvent) => {
const event = new CustomEvent('select-item', { cancelable: true })
event.altKey = realEvent.altKey
event.shiftKey = realEvent.shiftKey
event.ctrlKey = realEvent.ctrlKey
event.metaKey = realEvent.metaKey
return event
}
/**
* Set focus to first focusable element in preview
*/
const focusPreview = () => {
const previewDom = document.getElementById('preview')
const firstFocusable = previewDom.querySelector(focusableSelector)
if (firstFocusable) { firstFocusable.focus() }
}
/**
* Check if cursor in the end of input
*
* @param {DOMElement} input
*/
const cursorInEndOfInut = ({ selectionStart, selectionEnd, value }) => (
selectionStart === selectionEnd && selectionStart >= value.length
)
const electronWindow = remote.getCurrentWindow()
/**
* Set resizable and size for main electron window when results count is changed
*/
const updateElectronWindow = (results, visibleResults) => {
const { length } = results
const win = electronWindow
const [width] = win.getSize()
// When results list is empty window is not resizable
win.setResizable(length !== 0)
if (length === 0) {
win.setMinimumSize(WINDOW_WIDTH, INPUT_HEIGHT)
win.setSize(width, INPUT_HEIGHT)
const [x, y] = config.get('winPosition')
win.setPosition(x, y)
return
}
const resultHeight = Math.max(Math.min(visibleResults, length), MIN_VISIBLE_RESULTS)
const heightWithResults = resultHeight * RESULT_HEIGHT + INPUT_HEIGHT
const minHeightWithResults = MIN_VISIBLE_RESULTS * RESULT_HEIGHT + INPUT_HEIGHT
win.setMinimumSize(WINDOW_WIDTH, minHeightWithResults)
win.setSize(width, heightWithResults)
const [x, y] = config.get('winPosition')
win.setPosition(x, y)
}
const onDocumentKeydown = (event) => {
if (event.keyCode === 27) {
event.preventDefault()
document.getElementById('main-input').focus()
}
}
function Autocomplete({ autocompleteCalculator }) {
const autocompleteTerm = autocompleteCalculator()
return autocompleteTerm
? <div className={styles.autocomplete}>{autocompleteTerm}</div>
: null
}
Autocomplete.propTypes = {
autocompleteCalculator: PropTypes.func.isRequired,
}
/**
* Main search container
*
* TODO: Remove redux
* TODO: Split to more components
*/
function Cerebro({
results, selected, visibleResults, actions, term, prevTerm, statusBarText
}) {
const mainInput = useRef(null)
const [mainInputFocused, setMainInputFocused] = useState(false)
const [prevResultsLenght, setPrevResultsLenght] = useState(() => results.length)
const focusMainInput = () => {
mainInput.current.focus()
if (config.get('selectOnShow')) {
mainInput.current.select()
}
}
// suscribe to events
useEffect(() => {
focusMainInput()
updateElectronWindow(results, visibleResults)
// Listen for window.resize and change default space for results to user's value
window.addEventListener('resize', onWindowResize)
// Add some global key handlers
window.addEventListener('keydown', onDocumentKeydown)
// Cleanup event listeners on unload
// NOTE: when page refreshed (location.reload) componentWillUnmount is not called
window.addEventListener('beforeunload', cleanup)
electronWindow.on('show', focusMainInput)
electronWindow.on('show', () => updateElectronWindow(results, visibleResults))
// function to be called when unmounted
return () => {
cleanup()
}
}, [])
if (results.length !== prevResultsLenght) {
// Resize electron window when results count changed
updateElectronWindow(results, visibleResults)
setPrevResultsLenght(results.length)
}
/**
* Handle resize window and change count of visible results depends on window size
*/
const onWindowResize = () => {
if (results.length <= MIN_VISIBLE_RESULTS) return false
let maxVisibleResults = Math.floor((window.outerHeight - INPUT_HEIGHT) / RESULT_HEIGHT)
maxVisibleResults = Math.max(MIN_VISIBLE_RESULTS, maxVisibleResults)
if (maxVisibleResults !== visibleResults) {
actions.changeVisibleResults(maxVisibleResults)
}
}
/**
* Handle keyboard shortcuts
*/
const onKeyDown = (event) => {
const highlighted = highlightedResult()
// TODO: go to first result on cmd+up and last result on cmd+down
if (highlighted && highlighted.onKeyDown) highlighted.onKeyDown(event)
if (event.defaultPrevented) { return }
const keyActions = {
select: () => selectCurrent(event),
arrowRight: () => {
if (cursorInEndOfInut(event.target)) {
if (autocompleteValue()) {
// Autocomplete by arrow right only if autocomple value is shown
autocomplete(event)
} else {
focusPreview()
event.preventDefault()
}
}
},
arrowDown: () => {
actions.moveCursor(1)
event.preventDefault()
},
arrowUp: () => {
if (results.length > 0) {
actions.moveCursor(-1)
} else if (prevTerm) {
actions.updateTerm(prevTerm)
}
event.preventDefault()
}
}
// shortcuts for ctrl+...
if ((event.metaKey || event.ctrlKey) && !event.altKey) {
// Copy to clipboard on cmd+c
if (event.keyCode === 67) {
const text = highlightedResult()?.clipboard || term
if (text) {
clipboard.writeText(text)
actions.reset()
if (!event.defaultPrevented) {
electronWindow.hide()
}
event.preventDefault()
}
return
}
// Select text on cmd+a
if (event.keyCode === 65) {
mainInput.current.select()
event.preventDefault()
}
// Select element by number
if (event.keyCode >= 49 && event.keyCode <= 57) {
const number = Math.abs(49 - event.keyCode)
const result = results[number]
if (result) return selectItem(result, event)
}
// Lightweight vim-mode: cmd/ctrl + jklo
switch (event.keyCode) {
case 74:
keyActions.arrowDown()
break
case 75:
keyActions.arrowUp()
break
case 76:
keyActions.arrowRight()
break
case 79:
keyActions.select()
break
}
}
switch (event.keyCode) {
case 9:
autocomplete(event)
break
case 39:
keyActions.arrowRight()
break
case 40:
keyActions.arrowDown()
break
case 38:
keyActions.arrowUp()
break
case 13:
keyActions.select()
break
case 27:
actions.reset()
electronWindow.hide()
break
}
}
const onMainInputFocus = () => setMainInputFocused(true)
const onMainInputBlur = () => setMainInputFocused(false)
const cleanup = () => {
window.removeEventListener('resize', onWindowResize)
window.removeEventListener('keydown', onDocumentKeydown)
window.removeEventListener('beforeunload', cleanup)
electronWindow.removeAllListeners('show')
}
/**
* Get highlighted result
* @return {Object}
*/
const highlightedResult = () => results[selected]
/**
* Select item from results list
* @param {[type]} item [description]
* @return {[type]} [description]
*/
const selectItem = (item, realEvent) => {
actions.reset()
const event = wrapEvent(realEvent)
item.onSelect(event)
if (!event.defaultPrevented) electronWindow.hide()
}
/**
* Autocomple search term from highlighted result
*/
const autocomplete = (event) => {
const { term: highlightedTerm } = highlightedResult()
if (highlightedTerm && highlightedTerm !== term) {
actions.updateTerm(highlightedTerm)
event.preventDefault()
}
}
/**
* Select highlighted element
*/
const selectCurrent = (event) => selectItem(highlightedResult(), event)
const autocompleteValue = () => {
const selectedResult = highlightedResult()
if (selectedResult && selectedResult.term) {
const regexp = new RegExp(`^${escapeStringRegexp(term)}`, 'i')
if (selectedResult.term.match(regexp)) {
return selectedResult.term.replace(regexp, term)
}
}
return ''
}
return (
<div className={styles.search}>
<Autocomplete autocompleteCalculator={autocompleteValue} />
<div className={styles.inputWrapper}>
<input
placeholder={config.get('searchBarPlaceholder')}
type="text"
id="main-input"
ref={mainInput}
value={term}
className={styles.input}
onChange={(e) => actions.updateTerm(e.target.value)}
onKeyDown={onKeyDown}
onFocus={onMainInputFocus}
onBlur={onMainInputBlur}
/>
</div>
<ResultsList
results={results}
selected={selected}
visibleResults={visibleResults}
onItemHover={actions.selectElement}
onSelect={selectItem}
mainInputFocused={mainInputFocused}
/>
{statusBarText && <StatusBar value={statusBarText} />}
</div>
)
}
Cerebro.propTypes = {
actions: PropTypes.shape({
reset: PropTypes.func,
moveCursor: PropTypes.func,
updateTerm: PropTypes.func,
changeVisibleResults: PropTypes.func,
selectElement: PropTypes.func,
}),
results: PropTypes.array,
selected: PropTypes.number,
visibleResults: PropTypes.number,
term: PropTypes.string,
statusBarText: PropTypes.string,
prevTerm: PropTypes.string,
}
function mapStateToProps(state) {
return {
selected: state.search.selected,
results: state.search.resultIds.map((id) => state.search.resultsById[id]),
term: state.search.term,
statusBarText: state.statusBar.text,
prevTerm: state.search.prevTerm,
visibleResults: state.search.visibleResults,
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(searchActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cerebro)
================================================
FILE: app/main/components/Cerebro/styles.module.css
================================================
.search {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
}
.inputWrapper {
position: relative;
z-index: 2;
width: 100%;
height: 45px;
}
.autocomplete {
position: absolute;
z-index: 1;
width: 100%;
height: 45px;
font-size: 1.5em;
padding: 0 10px;
line-height: 46px;
box-sizing: border-box;
color: var(--secondary-font-color);
white-space: pre;
}
::-webkit-scrollbar {
height: var(--scroll-height);
width: var(--scroll-width);
background: var(--scroll-background);
-webkit-border-radius: 0;
}
::-webkit-scrollbar-track {
background: var(--scroll-track);
}
::-webkit-scrollbar-track:active {
background: var(--scroll-track-active);
}
::-webkit-scrollbar-track:hover {
background: var(--scroll-track-hover);
}
::-webkit-scrollbar-thumb {
background: var(--scroll-thumb);
-webkit-border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--scroll-thumb-hover);
}
::-webkit-scrollbar-thumb:active {
background: var(--scroll-thumb-active);
}
::-webkit-scrollbar-thumb:vertical {
min-height: 10px;
}
::-webkit-scrollbar-thumb:horizontal {
min-width: 10px;
}
.input {
width: 100%;
height: 45px;
color: var(--main-font-color);
font-family: var(--main-font);
font-size: 1.5em;
border: 0;
outline: none;
padding: 0 10px;
line-height: 60px;
box-sizing: border-box;
background: transparent;
white-space: nowrap;
-webkit-app-region: drag;
-webkit-user-select: none;
}
================================================
FILE: app/main/components/ResultsList/Row/index.tsx
================================================
import React from 'react'
import SmartIcon from '../../SmartIcon'
// @ts-ignore
import styles from './styles.module.css'
interface RowProps {
style?: any
title?: string
icon?: string
selected?: boolean
subtitle?: string
onSelect?: () => void
onMouseMove?: () => void
}
function Row({
selected, icon, title, onSelect, onMouseMove, subtitle, style
}: RowProps) {
const classNames = [styles.row, selected ? styles.selected : null].join(' ')
return (
<div
style={style}
className={classNames}
onClick={onSelect}
onMouseMove={onMouseMove}
onKeyDown={() => {}}
>
{icon && <SmartIcon path={icon} className={styles.icon} />}
<div className={styles.details}>
{title && <div className={styles.title}>{title}</div>}
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</div>
</div>
)
}
export default Row
================================================
FILE: app/main/components/ResultsList/Row/styles.module.css
================================================
/**
* TODO: colors should be moved to variables
*/
.row {
position: relative;
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: flex-start;
white-space: nowrap;
width: 100%;
cursor: pointer;
box-sizing: border-box;
height: 45px;
padding: 3px 5px;
align-items: center;
color: var(--main-font-color);
background: var(--result-background);
}
.icon {
max-height: 30px;
max-width: 30px;
margin-right: 5px;
}
.title {
font-size: .8em;
max-width: 100%;
/* overflow-x: hidden; */
color: var(--result-title-color);
}
.subtitle {
font-size: 0.8em;
font-weight: 300;
color: var(--result-subtitle-color);
max-width: 100%;
/* overflow-x: hidden; */
}
.selected {
background: var(--selected-result-background);
.title {
color: var(--selected-result-title-color);
}
.subtitle {
color: var(--selected-result-subtitle-color);
}
}
.details {
position: relative;
display: flex;
flex-grow: 2;
flex-wrap: wrap;
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 90%;
}
================================================
FILE: app/main/components/ResultsList/index.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import { List } from 'react-virtualized'
import { RESULT_HEIGHT } from 'main/constants/ui'
import Row from './Row'
import styles from './styles.module.css'
function ResultsList({
results, selected, visibleResults, onSelect, mainInputFocused, onItemHover
}) {
const rowRenderer = ({ index, key, style }) => {
const result = results[index]
const attrs = {
...result,
// TODO: think about events
// In some cases action should be executed and window should be closed
// In some cases we should autocomplete value
selected: index === selected,
onSelect: (event) => onSelect(result, event),
// Move selection to item under cursor
onMouseMove: (event) => {
const { movementX, movementY } = event.nativeEvent
if (index === selected || !mainInputFocused) return false
if (movementX || movementY) {
// Hover item only when we had real movement of mouse
// We should prevent changing of selection when user uses keyboard
onItemHover(index)
}
},
}
// Plugins supply additional props (onKeyDown, term, etc.), keep forwarding them.
// eslint-disable-next-line react/jsx-props-no-spreading
return <Row key={key} style={style} {...attrs} />
}
const renderPreview = () => {
const selectedResult = results[selected]
if (!selectedResult.getPreview) return null
const preview = selectedResult.getPreview()
if (typeof preview === 'string') {
// Fallback for html previews intead of react component
return <div dangerouslySetInnerHTML={{ __html: preview }} />
}
return preview
}
const classNames = [styles.resultsList, mainInputFocused ? styles.focused : styles.unfocused].join(' ')
if (results.length === 0) return null
return (
<div className={styles.wrapper}>
<List
className={classNames}
height={visibleResults * RESULT_HEIGHT}
overscanRowCount={2}
rowCount={results.length}
rowHeight={RESULT_HEIGHT}
rowRenderer={rowRenderer}
width={(results[selected] !== undefined && results[selected].getPreview) ? 250 : 10000}
scrollToIndex={selected}
// Disable accesebility of VirtualScroll by tab
tabIndex={null}
/>
<div className={styles.preview} id="preview">
{renderPreview()}
</div>
</div>
)
}
ResultsList.propTypes = {
results: PropTypes.array,
selected: PropTypes.number,
visibleResults: PropTypes.number,
onItemHover: PropTypes.func,
onSelect: PropTypes.func,
mainInputFocused: PropTypes.bool,
}
export default ResultsList
================================================
FILE: app/main/components/ResultsList/styles.module.css
================================================
.wrapper {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
border-top: var(--main-border);
height: 100%;
position: relative;
}
.unfocused {
opacity: .5;
}
.resultsList {
overflow-y: auto;
width: 100%;
min-width: 250px;
}
.preview {
flex-grow: 2;
padding: 10px 10px 20px 10px;
background-color: var(--main-background-color);
align-items: center;
display: flex;
max-height: 100%;
position: absolute;
left: 250px;
top: 0;
bottom: 0;
right: 0;
overflow: auto;
/*
Instead of using `justify-content: center` we have to use this hack.
In this case child element that is bigger than `.preview ` will be placed on left border
instead of moving outside of container
*/
&::before, &::after {
content: '';
margin: auto;
}
&:empty {
display: none;
}
input {
border: var(--preview-input-border);
background: var(--preview-input-background);
color: var(--preview-input-color);
}
:global {
/* Styles for react-select */
.Select {
.Select-control {
border: var(--preview-input-border);
background: var(--preview-input-background);
color: var(--preview-input-color);
}
.Select-menu-outer {
border: var(--preview-input-border);
background: var(--preview-input-background);
}
.Select-input input {
border: 0;
}
.Select-value-label {
color: var(--preview-input-color) !important;
}
.Select-option {
background: var(--preview-input-background);
color: var(--preview-input-color);
&.is-selected {
color: var(--selected-result-title-color);
background: var(--selected-result-background);
}
&.is-focused {
color: var(--selected-result-title-color);
background: var(--selected-result-background);
filter: opacity(50%);
}
}
.Select-option.is-selected {
}
}
}
}
================================================
FILE: app/main/components/SmartIcon/getFileIcon/index.ts
================================================
const { memoize } = require('cerebro-tools')
const empty = () => Promise.reject()
/* eslint-disable global-require */
/* eslint-disable import/no-mutable-exports */
let getFileIcon = empty
if (process.platform === 'darwin') {
getFileIcon = require('./mac')
}
if (process.platform === 'win32') {
getFileIcon = require('./windows')
}
module.exports = memoize(getFileIcon)
/* eslint-enable global-require */
/* eslint-disable import/no-mutable-exports */
================================================
FILE: app/main/components/SmartIcon/getFileIcon/mac.ts
================================================
const remote = require('@electron/remote')
/**
* Get system icon for file
*
* @param {String} path File path
* @param {Number} options.width
* @param {[type]} options.height
* @return {Promise<String>} Promise resolves base64-encoded source of icon
*/
module.exports = async function getFileIcon(path: string, { width = 24, height = 24 } = {}) {
// eslint-disable-next-line global-require
const plist = require('simple-plist')
if (!path.endsWith('.app') && !path.endsWith('.app/')) {
const icon = await remote.nativeImage.createThumbnailFromPath(path, { width, height })
return icon.toDataURL()
}
const { CFBundleIconFile } = plist.readFileSync(`${path}/Contents/Info.plist`)
if (!CFBundleIconFile) {
return null
}
const iconFileName = CFBundleIconFile.endsWith('.icns')
? CFBundleIconFile : `${CFBundleIconFile}.icns`
const icon = await remote.nativeImage
.createThumbnailFromPath(
`${path}/Contents/Resources/${iconFileName}`,
{ width, height }
)
return icon.toDataURL()
}
================================================
FILE: app/main/components/SmartIcon/getFileIcon/windows.ts
================================================
const remote = require('@electron/remote')
/**
* Get system icon for file
*
* @param {String} path File path
* @return {Promise<String>} Promise resolves base64-encoded source of icon
*/
module.exports = async function getFileIcon(path: string) {
const nativeIcon = await remote.app.getFileIcon(path)
return nativeIcon.toDataURL()
}
================================================
FILE: app/main/components/SmartIcon/index.tsx
================================================
import React, { memo } from 'react'
import FontAwesome from 'react-fontawesome'
// @ts-ignore
import getFileIcon from './getFileIcon'
interface IconProps {
className?: string
path: string
}
/**
* Check if provided string is an image src
* It can be a path to png/jpg/svg image or data-uri
*
* @param {String} path
* @return {Boolean}
*/
const isImage = (path: string) => !!path.match(/(^data:)|(\.(png|jpe?g|svg|ico)$)/)
/**
* Check if provided string matches a FontAwesome icon
*/
const isFontAwesome = (path: string) => path.match(/^fa-(.+)$/)
/**
* Render icon for provided path.
* It will render the same icon, that you see in Finder
*
* @param {String} options.className
* @param {String} options.path
* @return {Function}
*/
function FileIcon({ className, path }:IconProps) {
const src = getFileIcon(path)
return src ? <img src={src} alt="" className={className} /> : null
}
/**
* This component renders:
* – if `options.path` is an image this image will be rendered. Supported formats are:
* png, jpg, svg and icns
* - otherwise it will render icon for provided path, that you can see in Finder
* @param {String} options.className
* @param {String} options.path
* @return {Function}
*/
function SmartIcon({ className, path }: IconProps) {
const fontAwesomeMatches = isFontAwesome(path)
if (fontAwesomeMatches) {
return (
<FontAwesome
name={fontAwesomeMatches[1]}
size="2x"
className={className}
/>
)
}
return (
isImage(path)
? <img src={path} alt={path} className={className} />
: <FileIcon path={path} className={className} />
)
}
export default memo(SmartIcon)
================================================
FILE: app/main/components/StatusBar/index.tsx
================================================
import React from 'react'
// @ts-ignore
import styles from './styles.module.css'
interface StatusBarProps {
value?: string
}
function StatusBar({ value }: StatusBarProps) {
return <div className={styles.statusBar}>{value}</div>
}
export default StatusBar
================================================
FILE: app/main/components/StatusBar/styles.module.css
================================================
.statusBar {
position: absolute;
bottom: 0;
right: 0;
padding: 5px;
border-radius: 5px 0 0 0;
border: var(--main-border);
color: var(--secondary-font-color);
background: var(--preview-input-background);
border-width: 1px 0 0 1px;
font-size: .75em;
}
================================================
FILE: app/main/constants/actionTypes.ts
================================================
export const UPDATE_TERM = 'UPDATE_TERM'
export const MOVE_CURSOR = 'MOVE_CURSOR'
export const SELECT_ELEMENT = 'SELECT_ELEMENT'
export const SHOW_RESULT = 'SHOW_RESULT'
export const HIDE_RESULT = 'HIDE_RESULT'
export const UPDATE_RESULT = 'UPDATE_RESULT'
export const RESET = 'RESET'
export const CHANGE_VISIBLE_RESULTS = 'CHANGE_VISIBLE_RESULTS'
export const ICON_LOADED = 'ICON_LOADED'
export const SET_STATUS_BAR_TEXT = 'SET_STATUS_BAR_TEXT'
================================================
FILE: app/main/constants/ui.ts
================================================
// Height of main input
export const INPUT_HEIGHT = 45
// Heigth of default result line
export const RESULT_HEIGHT = 45
// Width of main window
export const WINDOW_WIDTH = 650
// Maximum results that would be rendered
export const MAX_RESULTS = 25
// Results view shows this count of resutls without scrollbar
export const MIN_VISIBLE_RESULTS = 10
================================================
FILE: app/main/createWindow/AppTray.js
================================================
import { Menu, Tray, app } from 'electron'
import showWindowWithTerm from './showWindowWithTerm'
import toggleWindow from './toggleWindow'
import checkForUpdates from './checkForUpdates'
/**
* Class that controls state of icon in menu bar
*/
export default class AppTray {
/**
* @param {String} options.src Absolute path for tray icon
* @param {Function} options.isDev Development mode or not
* @param {BrowserWindow} options.mainWindow
* @param {BrowserWindow} options.backgroundWindow
* @return {AppTray}
*/
constructor(options) {
this.tray = null
this.options = options
}
/**
* Show application icon in menu bar
*/
show() {
const tray = new Tray(this.options.src)
tray.setToolTip('Cerebro')
tray.setContextMenu(this.buildMenu())
this.tray = tray
}
setIsDev(isDev) {
this.options.isDev = isDev
if (this.tray) {
this.tray.setContextMenu(this.buildMenu())
}
}
buildMenu() {
const { mainWindow, backgroundWindow, isDev } = this.options
const separator = { type: 'separator' }
const template = [
{
label: 'Toggle Cerebro',
click: () => toggleWindow(mainWindow)
},
separator,
{
label: 'Plugins',
click: () => showWindowWithTerm(mainWindow, 'plugins'),
},
{
label: 'Preferences...',
click: () => showWindowWithTerm(mainWindow, 'Cerebro Settings'),
},
separator,
{
label: 'Check for updates',
click: checkForUpdates,
},
separator,
]
if (isDev) {
template.push(separator)
template.push({
label: 'Development',
submenu: [
{
label: 'DevTools (main)',
accelerator: 'CmdOrCtrl+Shift+I',
click: () => mainWindow.webContents.openDevTools({ mode: 'detach' })
},
{
label: 'DevTools (background)',
accelerator: 'CmdOrCtrl+Shift+B',
click: () => backgroundWindow.webContents.openDevTools({ mode: 'detach' })
},
{
label: 'Reload',
click: () => {
app.relaunch()
app.exit()
}
}]
})
}
template.push(separator)
template.push({
label: 'Quit Cerebro',
click: () => app.quit()
})
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
return menu
}
/**
* Hide icon in menu bar
*/
hide() {
if (this.tray) {
this.tray.destroy()
this.tray = null
}
}
}
================================================
FILE: app/main/createWindow/autoStart.js
================================================
import { app } from 'electron'
import AutoLaunch from 'auto-launch'
const isLinux = !['win32', 'darwin'].includes(process.platform)
const isDevelopment = process.env.NODE_ENV === 'development'
const appLauncher = isLinux
? new AutoLaunch({ name: 'Cerebro' })
: null
const isEnabled = async () => (
isLinux
? appLauncher.isEnabled()
: app.getLoginItemSettings().openAtLogin
)
const set = async (openAtLogin) => {
const openAtStartUp = openAtLogin && !isDevelopment
if (isLinux) {
return openAtStartUp
? appLauncher.enable()
: appLauncher.disable()
}
return app.setLoginItemSettings({ openAtLogin: openAtStartUp })
}
export default { isEnabled, set }
================================================
FILE: app/main/createWindow/buildMenu.js
================================================
import { Menu, shell, app } from 'electron'
export default (mainWindow) => {
const template = [{
label: 'Electron',
submenu: [{
label: 'About ElectronReact',
selector: 'orderFrontStandardAboutPanel:'
},
{ type: 'separator' },
{
label: 'Services',
submenu: []
},
{ type: 'separator' },
{
label: 'Hide ElectronReact',
accelerator: 'Command+H',
selector: 'hide:'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:'
},
{
label: 'Show All',
selector: 'unhideAllApplications:'
},
{ type: 'separator' },
{
label: 'Quit',
accelerator: 'Command+Q',
click() { app.quit() }
}]
},
{
label: 'Edit',
submenu: [{
label: 'Undo',
accelerator: 'Command+Z',
selector: 'undo:'
},
{
label: 'Redo',
accelerator: 'Shift+Command+Z',
selector: 'redo:'
},
{ type: 'separator' },
{
label: 'Cut',
accelerator: 'Command+X',
selector: 'cut:'
},
{
label: 'Copy',
accelerator: 'Command+C',
selector: 'copy:'
},
{
label: 'Paste',
accelerator: 'Command+V',
selector: 'paste:'
},
{
label: 'Select All',
accelerator: 'Command+A',
selector: 'selectAll:'
}]
},
{
label: 'View',
submenu: [{
label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F',
click() {
mainWindow.setFullScreen(!mainWindow.isFullScreen())
}
}]
},
{
label: 'Window',
submenu: [{
label: 'Minimize',
accelerator: 'Command+M',
selector: 'performMiniaturize:'
},
{
label: 'Close',
accelerator: 'Command+W',
selector: 'performClose:'
},
{ type: 'separator' },
{
label: 'Bring All to Front',
selector: 'arrangeInFront:'
}]
},
{
label: 'Help',
submenu: [{
label: 'Learn More',
click() { shell.openExternal('http://electron.atom.io') }
},
{
label: 'Documentation',
click() { shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme') }
},
{
label: 'Community Discussions',
click() { shell.openExternal('https://discuss.atom.io/c/electron') }
},
{
label: 'Search Issues',
click() { shell.openExternal('https://github.com/atom/electron/issues') }
}]
}]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
================================================
FILE: app/main/createWindow/checkForUpdates.js
================================================
import { dialog, app, shell } from 'electron'
import { autoUpdater } from 'electron-updater'
const currentVersion = app.getVersion()
const DEFAULT_DOWNLOAD_URL = 'https://github.com/cerebroapp/cerebro/releases'
const TITLE = 'Cerebro Updates'
const PLATFORM_EXTENSIONS = {
darwin: 'dmg',
linux: 'AppImage',
win32: 'exe'
}
const { platform } = process
const installerExtension = PLATFORM_EXTENSIONS[platform]
const findInstaller = (assets) => {
if (!installerExtension) return DEFAULT_DOWNLOAD_URL
const regexp = new RegExp(`\.${installerExtension}$`)
const downloadUrl = assets
.find(({ url }) => url.match(regexp))
return downloadUrl || DEFAULT_DOWNLOAD_URL
}
export default async () => {
try {
const release = await autoUpdater.checkForUpdates()
if (release) {
const { updateInfo: { version, files } } = release
dialog.showMessageBox({
buttons: ['Skip', 'Download'],
defaultId: 1,
cancelId: 0,
title: TITLE,
message: `New version available: ${version}`,
detail: 'Click download to get it now',
}, (response) => {
if (response === 1) {
const url = findInstaller(files)
shell.openExternal(url)
}
})
} else {
dialog.showMessageBox({
title: TITLE,
message: `You are using latest version of Cerebro (${currentVersion})`,
buttons: []
})
}
} catch (err) {
console.log('Catch error!', err)
dialog.showErrorBox(TITLE, 'Error fetching latest version')
}
}
================================================
FILE: app/main/createWindow/handleUrl.js
================================================
import { parse } from 'url'
import showWindowWithTerm from './showWindowWithTerm'
export default (mainWindow, url) => {
const { host: action, query } = parse(url, { parseQueryString: true })
// Currently only search action supported.
// We can extend this handler to support more
// like `plugins/install` or do something plugin-related
if (action === 'search') {
showWindowWithTerm(mainWindow, query.term)
} else {
showWindowWithTerm(mainWindow, url)
}
}
================================================
FILE: app/main/createWindow/showWindowWithTerm.ts
================================================
/**
* Show main window with updated search term
*
* @return {BrowserWindow} appWindow
*/
export default (appWindow: any, term: string) => {
appWindow.show()
appWindow.focus()
appWindow.webContents.send('message', {
message: 'showTerm',
payload: term
})
}
================================================
FILE: app/main/createWindow/toggleWindow.ts
================================================
/**
* Show or hide main window
* @return {BrowserWindow} appWindow
*/
export default (appWindow: any) => {
if (appWindow.isVisible()) {
appWindow.blur() // once for blurring the content of the window(?)
appWindow.blur() // twice somehow restores focus to prev foreground window
appWindow.hide()
} else {
appWindow.show()
appWindow.focus()
}
}
================================================
FILE: app/main/createWindow.js
================================================
import {
BrowserWindow, globalShortcut, app, shell
} from 'electron'
import debounce from 'lodash/debounce'
import EventEmitter from 'events'
import config from 'lib/config'
import {
INPUT_HEIGHT,
WINDOW_WIDTH
} from './constants/ui'
import buildMenu from './createWindow/buildMenu'
import toggleWindow from './createWindow/toggleWindow'
import handleUrl from './createWindow/handleUrl'
export default ({ src, isDev }) => {
const [x, y] = config.get('winPosition')
const browserWindowOptions = {
width: WINDOW_WIDTH,
minWidth: WINDOW_WIDTH,
height: INPUT_HEIGHT,
x,
y,
frame: false,
resizable: false,
transparent: true,
show: config.get('firstStart'),
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: false,
enableRemoteModule: true,
contextIsolation: false
},
// Show main window on launch only when application started for the first time
}
if (process.platform === 'linux') {
browserWindowOptions.type = 'splash'
}
const mainWindow = new BrowserWindow(browserWindowOptions)
// Workaround to set the position the first time (centers the window)
config.set('winPosition', mainWindow.getPosition())
// Float main window above full-screen apps
mainWindow.setAlwaysOnTop(true, 'modal-panel')
mainWindow.loadURL(src)
mainWindow.settingsChanges = new EventEmitter()
// Get global shortcut from app settings
let shortcut = config.get('hotkey')
// Function to toggle main window
const toggleMainWindow = () => toggleWindow(mainWindow)
// Function to show main window
const showMainWindow = () => {
mainWindow.show()
mainWindow.focus()
}
// Setup event listeners for main window
globalShortcut.register(shortcut, toggleMainWindow)
mainWindow.on('blur', () => {
if (!isDev() && config.get('hideOnBlur')) {
// Hide window on blur in production
// In development we usually use developer tools that can blur a window
mainWindow.hide()
}
})
// Save window position when it is being moved
mainWindow.on('move', debounce(() => {
if (!mainWindow.isVisible()) {
return
}
config.set('winPosition', mainWindow.getPosition())
}, 100))
mainWindow.on('close', app.quit)
mainWindow.webContents.on('new-window', (event, url) => {
shell.openExternal(url)
event.preventDefault()
})
mainWindow.webContents.on('will-navigate', (event, url) => {
if (url !== mainWindow.webContents.getURL()) {
shell.openExternal(url)
event.preventDefault()
}
})
// Change global hotkey if it is changed in app settings
mainWindow.settingsChanges.on('hotkey', (value) => {
globalShortcut.unregister(shortcut)
shortcut = value
globalShortcut.register(shortcut, toggleMainWindow)
})
// Change theme css file
mainWindow.settingsChanges.on('theme', (value) => {
mainWindow.webContents.send('message', {
message: 'updateTheme',
payload: value
})
})
mainWindow.settingsChanges.on('proxy', (value) => {
mainWindow.webContents.session.setProxy({
proxyRules: value
})
})
// Handle window.hide: if cleanOnHide value in preferences is true
// we clear all results and show empty window every time
const resetResults = () => {
mainWindow.webContents.send('message', {
message: 'showTerm',
payload: ''
})
}
// Handle change of cleanOnHide value in settins
const handleCleanOnHideChange = (value) => {
if (value) {
mainWindow.on('hide', resetResults)
} else {
mainWindow.removeListener('hide', resetResults)
}
}
// Set or remove handler when settings changed
mainWindow.settingsChanges.on('cleanOnHide', handleCleanOnHideChange)
// Set initial handler if it is needed
handleCleanOnHideChange(config.get('cleanOnHide'))
// Restore focus in previous application
// MacOS only: https://github.com/electron/electron/blob/master/docs/api/app.md#apphide-macos
if (process.platform === 'darwin') {
mainWindow.on('hide', () => {
app.hide()
})
}
// Show main window when user opens application, but it is already opened
app.on('open-file', (event, path) => handleUrl(mainWindow, path))
app.on('open-url', (event, path) => handleUrl(mainWindow, path))
app.on('activate', showMainWindow)
// Someone tried to run a second instance, we should focus our window.
const shouldQuit = app.requestSingleInstanceLock()
if (!shouldQuit) {
app.quit()
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
})
}
// Save in config information, that application has been started
config.set('firstStart', false)
buildMenu(mainWindow)
return mainWindow
}
================================================
FILE: app/main/css/global.css
================================================
@import "system-font.css";
@import url("~normalize.css/normalize.css");
@import url("~react-virtualized/styles.css");
html,
body {
margin: 0;
padding: 0;
background-color: var(--main-background-color);
color: var(--main-font-color);
}
body {
position: relative;
height: 100vh;
font-family: var(--main-font);
overflow-y: hidden;
}
#root {
height: 100%;
}
================================================
FILE: app/main/css/system-font.css
================================================
/*! system-font.css v1.1.0 | CC0-1.0 License | github.com/jonathantneal/system-font-face */
@font-face {
font-family: system;
font-style: normal;
font-weight: 300;
src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 300;
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");
}
@font-face {
font-family: system;
font-style: normal;
font-weight: 400;
src: local(".SFNSText-Regular"), local(".HelveticaNeueDeskInterface-Regular"), local(".LucidaGrandeUI"), local("Ubuntu"), local("Segoe UI"), local("Roboto-Regular"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 400;
src: local(".SFNSText-Italic"), local(".HelveticaNeueDeskInterface-Italic"), local(".LucidaGrandeUI"), local("Ubuntu Italic"), local("Segoe UI Italic"), local("Roboto-Italic"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: normal;
font-weight: 500;
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");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 500;
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");
}
@font-face {
font-family: system;
font-style: normal;
font-weight: 700;
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");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 700;
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");
}
================================================
FILE: app/main/css/themes/dark.css
================================================
:root {
/* Main fonts and colors */
--main-background-color: rgba(62, 65, 67, 1);
--main-font: system, sans-serif;
--main-font-color: white;
/* border styles */
--main-border: 1px solid #686869;
/* Secondary fonts and colors */
--secondary-font-color: #9B9D9F;
/* results list */
--result-background: transparent;
--result-title-color: var(--main-font-color);
--result-subtitle-color: #cccccc;
/* selected result */
--selected-result-title-color: white;
--selected-result-subtitle-color: var(--result-subtitle-color);
--selected-result-background: #1972D6;
/* scrollbar */
--scroll-background: var(--main-background-color);
--scroll-track: #2E2E2C;
--scroll-track-active: var(--scroll-track);
--scroll-track-hover: var(--scroll-track);
--scroll-thumb: var(--secondary-font-color);
--scroll-thumb-hover: var(--scroll-thumb);
--scroll-thumb-active: var(--main-font-color);
--scroll-width: 5px;
--scroll-height: 5px;
/* inputs */
--preview-input-background: #2E2E2C;
--preview-input-color: var(--main-font-color);
--preview-input-border: 0;
/* filter for previews */
--preview-filter: invert(100%) hue-rotate(180deg) contrast(80%);
}
================================================
FILE: app/main/css/themes/light.css
================================================
:root {
/* Main fonts and colors */
--main-background-color: rgba(255, 255, 255, 1);
--main-font: system, sans-serif;
--main-font-color: #000000;
/* border styles */
--main-border: 1px solid #eee;
/* Secondary fonts and colors */
--secondary-font-color: #999;
/* results list */
--result-background: transparent;
--result-title-color: var(--main-font-color);
--result-subtitle-color: #cccccc;
/* selected result */
--selected-result-title-color: white;
--selected-result-subtitle-color: var(--result-subtitle-color);
--selected-result-background: rgba(18, 110, 219, 1);
/* scrollbar */
--scroll-background: var(--main-background-color);
--scroll-track: #e0e0e0;
--scroll-track-active: var(--scroll-track);
--scroll-track-hover: var(--scroll-track);
--scroll-thumb: var(--secondary-font-color);
--scroll-thumb-hover: var(--scroll-thumb);
--scroll-thumb-active: var(--main-font-color);
--scroll-width: 5px;
--scroll-height: 5px;
/* inputs */
--preview-input-background: white;
--preview-input-color: var(--main-font-color);
--preview-input-border: 1px solid #ccc;
/* filter for previews */
--preview-filter: none;
}
================================================
FILE: app/main/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="script-src * 'unsafe-inline' 'unsafe-eval';"/>
<title>Cerebro</title>
<link rel='stylesheet' id='cerebro-theme'>
<script>
(function() {
// NODE_ENV is undefined in production
if (!process.env.NODE_ENV) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '../dist/main.css';
document.write(link.outerHTML);
}
}());
</script>
</head>
<body>
<div id="root">
</div>
<script>
(function() {
const script = document.createElement('script');
script.async = true;
script.src = process.env.NODE_ENV === 'development'
? 'http://localhost:3000/dist/main.bundle.js'
: '../dist/main.bundle.js';
document.write(script.outerHTML);
}());
</script>
</body>
</html>
================================================
FILE: app/main/main.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import initializePlugins from 'lib/initializePlugins'
import { on } from 'lib/rpc'
import config from 'lib/config'
import { updateTerm } from './actions/search'
import store from './store'
import Cerebro from './components/Cerebro'
import './css/global.css'
global.React = React
global.ReactDOM = ReactDOM
global.isBackground = false
/**
* Change current theme
*
* @param {String} src Absolute path to new theme css file
*/
const changeTheme = (src) => {
document.getElementById('cerebro-theme').href = src
}
// Set theme from config
changeTheme(config.get('theme'))
// Render main container
ReactDOM.render(
<Provider store={store}>
<Cerebro />
</Provider>,
document.getElementById('root')
)
// Initialize plugins
initializePlugins()
// Handle `showTerm` rpc event and replace search term with payload
on('showTerm', (term) => store.dispatch(updateTerm(term)))
on('update-downloaded', () => (
new Notification('Cerebro: update is ready to install', {
body: 'New version is downloaded and will be automatically installed on quit'
})
))
// Handle `updateTheme` rpc event and change current theme
on('updateTheme', changeTheme)
================================================
FILE: app/main/reducers/index.js
================================================
import { combineReducers } from 'redux'
import search from './search'
import statusBar from './statusBar'
const rootReducer = combineReducers({
search,
statusBar
})
export default rootReducer
================================================
FILE: app/main/reducers/search.js
================================================
/* eslint no-shadow: [2, { "allow": ["comments"] }] */
import uniq from 'lodash/uniq'
import orderBy from 'lodash/orderBy'
import {
UPDATE_TERM,
MOVE_CURSOR,
SELECT_ELEMENT,
SHOW_RESULT,
HIDE_RESULT,
UPDATE_RESULT,
RESET,
CHANGE_VISIBLE_RESULTS
} from 'main/constants/actionTypes'
import { MIN_VISIBLE_RESULTS } from 'main/constants/ui'
const initialState = {
// Search term in main input
term: '',
// Store last used term in separate field
prevTerm: '',
// Array of ids of results
resultIds: [],
resultsById: {},
// Index of selected result
selected: 0,
// Count of visible results
visibleResults: MIN_VISIBLE_RESULTS
}
/**
* Normalize index of selected item.
* Index should be >= 0 and <= results.length
*
* @param {Integer} index
* @param {Integer} length current count of found results
* @return {Integer} normalized index
*/
function normalizeSelection(index, length) {
const normalizedIndex = index % length
return normalizedIndex < 0 ? length + normalizedIndex : normalizedIndex
}
// Function that does nothing
const noon = () => {}
function normalizeResult(result) {
return {
...result,
onFocus: result.onFocus || noon,
onBlur: result.onFocus || noon,
onSelect: result.onSelect || noon,
}
}
export default function search(stateParam, action) {
const state = stateParam === undefined ? initialState : stateParam
const { type, payload } = action
switch (type) {
case UPDATE_TERM: {
return {
...state,
term: payload,
resultIds: [],
selected: 0
}
}
case MOVE_CURSOR: {
const { selected: currentSelected, resultIds } = state
const nextSelected = normalizeSelection(currentSelected + payload, resultIds.length)
return {
...state,
selected: nextSelected,
}
}
case SELECT_ELEMENT: {
const selected = normalizeSelection(payload, state.resultIds.length)
return {
...state,
selected,
}
}
case UPDATE_RESULT: {
const { id, result } = payload
const { resultsById } = state
const newResult = {
...resultsById[id],
...result
}
return {
...state,
resultsById: {
...resultsById,
[id]: newResult
}
}
}
case HIDE_RESULT: {
const { id } = payload
let { resultsById, resultIds } = state
resultIds = resultIds.filter((resultId) => resultId !== id)
resultsById = resultIds.reduce((acc, resultId) => ({
...acc,
[resultId]: resultsById[resultId]
}), {})
return {
...state,
resultsById,
resultIds
}
}
case SHOW_RESULT: {
const { term, result } = payload
if (term !== state.term) {
// Do not show this result if term was changed
return state
}
let { resultsById, resultIds } = state
result.forEach((res) => {
resultsById = {
...resultsById,
[res.id]: normalizeResult(res)
}
resultIds = [...resultIds, res.id]
})
return {
...state,
resultsById,
resultIds: orderBy(uniq(resultIds), (id) => resultsById[id].order || 0)
}
}
case CHANGE_VISIBLE_RESULTS: {
return {
...state,
visibleResults: payload,
}
}
case RESET: {
return {
// Do not override last used search term with empty string
...state,
prevTerm: state.term || state.prevTerm,
resultsById: {},
resultIds: [],
term: '',
selected: 0,
}
}
default:
return state
}
}
================================================
FILE: app/main/reducers/statusBar.js
================================================
/* eslint no-shadow: [2, { "allow": ["comments"] }] */
import {
SET_STATUS_BAR_TEXT
} from 'main/constants/actionTypes'
const initialState = {
text: null
}
export default function search(stateParam, action) {
const state = stateParam === undefined ? initialState : stateParam
const { type, payload } = action
switch (type) {
case SET_STATUS_BAR_TEXT: {
return {
...state,
text: payload
}
}
default:
return state
}
}
================================================
FILE: app/main/store/configureStore.js
================================================
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'
const enhancer = applyMiddleware(thunk)
export default function configureStore(initialState) {
return createStore(rootReducer, initialState, enhancer)
}
================================================
FILE: app/main/store/index.ts
================================================
import configureStore from './configureStore'
export default configureStore()
================================================
FILE: app/main.development.js
================================================
import { app, ipcMain } from 'electron'
import path from 'path'
import createMainWindow from './main/createWindow'
import createBackgroundWindow from './background/createWindow'
import config from './lib/config'
import AppTray from './main/createWindow/AppTray'
import autoStart from './main/createWindow/autoStart'
import initAutoUpdater from './initAutoUpdater'
import {
WINDOW_WIDTH,
} from 'main/constants/ui'
const iconSrc = {
DEFAULT: `${__dirname}/tray_icon.png`,
darwin: `${__dirname}/tray_iconTemplate@2x.png`,
win32: `${__dirname}/tray_icon.ico`
}
const trayIconSrc = iconSrc[process.platform] || iconSrc.DEFAULT
const isDev = () => (process.env.NODE_ENV === 'development' || config.get('developerMode'))
let mainWindow
let backgroundWindow
let tray
const setupEnvVariables = () => {
process.env.CEREBRO_VERSION = app.getVersion()
const isPortableMode = process.argv.some((arg) => arg.toLowerCase() === '-p' || arg.toLowerCase() === '--portable')
// initiate portable mode
// set data directory to ./userdata
if (isPortableMode) {
const userDataPath = path.join(process.cwd(), 'userdata')
app.setPath('userData', userDataPath)
process.env.CEREBRO_DATA_PATH = userDataPath
} else {
process.env.CEREBRO_DATA_PATH = app.getPath('userData')
}
}
app.whenReady().then(() => {
// We cannot require the screen module until the app is ready.
const { screen } = require('electron')
setupEnvVariables()
mainWindow = createMainWindow({
isDev,
src: `file://${__dirname}/main/index.html`, // Main window html
})
mainWindow.on('show', (event) => {
const cursorScreenPoint = screen.getCursorScreenPoint()
const nearestDisplay = screen.getDisplayNearestPoint(cursorScreenPoint)
const goalWidth = WINDOW_WIDTH
const goalX = Math.floor(nearestDisplay.bounds.x + (nearestDisplay.size.width - goalWidth) / 2)
const goalY = nearestDisplay.bounds.y + 200 // "top" is hardcoded now, should get from config or calculate accordingly?
config.set('winPosition', [goalX, goalY])
})
// eslint-disable-next-line global-require
require('@electron/remote/main').initialize()
// eslint-disable-next-line global-require
require('@electron/remote/main').enable(mainWindow.webContents)
backgroundWindow = createBackgroundWindow({
src: `file://${__dirname}/background/index.html`,
})
// eslint-disable-next-line global-require
require('@electron/remote/main').enable(backgroundWindow.webContents)
tray = new AppTray({
src: trayIconSrc,
isDev: isDev(),
mainWindow,
backgroundWindow,
})
// Show tray icon if it is set in configuration
if (config.get('showInTray')) { tray.show() }
autoStart.isEnabled().then((enabled) => {
if (config.get('openAtLogin') !== enabled) {
autoStart.set(config.get('openAtLogin'))
}
})
initAutoUpdater(mainWindow)
app?.dock?.hide()
})
ipcMain.on('message', (event, payload) => {
const toWindow = event.sender === mainWindow.webContents ? backgroundWindow : mainWindow
toWindow.webContents.send('message', payload)
})
ipcMain.on('updateSettings', (event, key, value) => {
mainWindow.settingsChanges.emit(key, value)
// Show or hide menu bar icon when it is changed in setting
if (key === 'showInTray') {
value
? tray.show()
: tray.hide()
}
// Show or hide "development" section in tray menu
if (key === 'developerMode') { tray.setIsDev(isDev()) }
// Enable or disable auto start
if (key === 'openAtLogin') {
autoStart.isEnabled().then((enabled) => {
if (value !== enabled) autoStart.set(value)
})
}
})
ipcMain.on('quit', () => app.quit())
ipcMain.on('reload', () => {
app.relaunch()
app.exit()
})
================================================
FILE: app/package.json
================================================
{
"name": "cerebro",
"productName": "Cerebro",
"description": "Cerebro is an open-source launcher to improve your productivity and efficiency",
"version": "0.11.0",
"main": "./main.js",
"license": "MIT",
"author": {
"name": "CerebroApp Organization",
"email": "kelionweb@gmail.com",
"url": "https://github.com/cerebroapp"
},
"contributors": [
"Alexandr Subbotin <kelionweb@gmail.com> (https://github.com/KELiON)",
"Gustavo Pereira <oguhpereira@protonmail.com (https://github.com/oguhpereira)"
],
"dependencies": {
"@cerebroapp/cerebro-ui": "2.0.0-alpha.5",
"@electron/remote": "2.0.9",
"auto-launch": "5.0.5",
"cerebro-tools": "0.1.8",
"chokidar": "3.5.3",
"electron-store": "8.1.0",
"electron-updater": "5.3.0",
"escape-string-regexp": "5.0.0",
"fs-extra": "11.1.0",
"lodash": "4.17.23",
"normalize.css": "8.0.1",
"prop-types": "15.8.1",
"react": "18.2.0",
"react-addons-shallow-compare": "15.6.3",
"react-dom": "18.2.0",
"react-fontawesome": "^1.7.1",
"react-markdown": "8.0.5",
"react-redux": "8.0.5",
"react-virtualized": "9.22.3",
"redux": "4.2.1",
"redux-thunk": "2.4.2",
"semver": "7.3.8",
"simple-plist": "^1.1.1",
"tar-fs": "2.1.4"
},
"optionalDependencies": {},
"devDependencies": {
"@types/react": "^18.0.28"
}
}
================================================
FILE: app/plugins/core/autocomplete/index.js
================================================
import { search } from 'cerebro-tools'
import {
flow, filter, map, partialRight, values
} from 'lodash/fp'
import externalPlugins from 'plugins/externalPlugins'
import quit from 'plugins/core/quit'
import plugins from 'plugins/core/plugins'
import settings from 'plugins/core/settings'
import version from 'plugins/core/version'
import reload from 'plugins/core/reload'
const allPlugins = {
quit, plugins, settings, version, reload, ...externalPlugins
}
const toString = (plugin) => plugin.keyword
const notMatch = (term) => (plugin) => (
plugin.keyword !== term && `${plugin.keyword} ` !== term
)
const pluginToResult = (actions) => (res) => ({
title: res.name,
icon: res.icon,
term: `${res.keyword} `,
onSelect: (event) => {
event.preventDefault()
actions.replaceTerm(`${res.keyword} `)
}
})
/**
* Plugin for autocomplete other plugins
*
* @param {String} options.term
* @param {Function} options.display
*/
const fn = ({ term, display, actions }) => flow(
values,
filter((plugin) => !!plugin.keyword),
partialRight(search, [term, toString]),
filter(notMatch(term)),
map(pluginToResult(actions)),
display
)(allPlugins)
export default { fn, name: 'Plugins autocomplete' }
================================================
FILE: app/plugins/core/index.ts
================================================
import autocomplete from './autocomplete'
import quit from './quit'
import plugins from './plugins'
import settings from './settings'
import version from './version'
import reload from './reload'
export default {
autocomplete, quit, plugins, settings, version, reload
}
================================================
FILE: app/plugins/core/plugins/Preview/ActionButton.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import { KeyboardNavItem } from '@cerebroapp/cerebro-ui'
function ActionButton({ action, onComplete, text }) {
const onSelect = () => {
Promise.all(action()).then(onComplete)
}
return (
<KeyboardNavItem onSelect={onSelect}>
{text}
</KeyboardNavItem>
)
}
ActionButton.propTypes = {
action: PropTypes.func.isRequired,
text: PropTypes.string.isRequired,
onComplete: PropTypes.func.isRequired,
}
export default ActionButton
================================================
FILE: app/plugins/core/plugins/Preview/FormItem.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import { FormComponents } from '@cerebroapp/cerebro-ui'
const { Checkbox, Select, Text } = FormComponents
const components = {
bool: Checkbox,
option: Select,
}
function FormItem({
type, value, options, ...props
}) {
const Component = components[type] || Text
let actualValue = value
if (Component === Select) {
// when the value is a string, we need to find the option that matches it
if (typeof value === 'string' && options) {
actualValue = options.find((option) => option.value === value)
}
}
// Forward UI handlers from plugin configs (onChange, onBlur, etc.).
// eslint-disable-next-line react/jsx-props-no-spreading
return <Component type={type} value={actualValue} options={options} {...props} />
}
FormItem.propTypes = {
value: PropTypes.any,
type: PropTypes.string.isRequired,
options: PropTypes.array
}
export default FormItem
================================================
FILE: app/plugins/core/plugins/Preview/Settings.js
================================================
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import config from 'lib/config'
import FormItem from './FormItem'
import styles from './styles.module.css'
function Settings({ settings, name }) {
const [values, setValues] = useState(() => config.get('plugins')[name] || {})
useEffect(() => {
config.set('plugins', {
...config.get('plugins'),
[name]: values,
})
}, [values])
const changeSetting = async (label, value) => {
setValues((prev) => ({
...prev,
[label]: value
}))
}
const renderSetting = (key) => {
const setting = settings[key]
const { defaultValue, label, ...restProps } = setting
const value = values[key] || defaultValue
return (
<FormItem
key={key}
label={label || key}
value={value}
onChange={(newValue) => changeSetting(key, newValue)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...restProps}
/>
)
}
return (
<div className={styles.settingsWrapper}>
{ Object.keys(settings).map(renderSetting) }
</div>
)
}
export default Settings
Settings.propTypes = {
name: PropTypes.string.isRequired,
settings: PropTypes.object.isRequired,
}
================================================
FILE: app/plugins/core/plugins/Preview/index.js
================================================
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { KeyboardNav, KeyboardNavItem } from '@cerebroapp/cerebro-ui'
import { client } from 'lib/plugins'
import ReactMarkdown from 'react-markdown'
import ActionButton from './ActionButton.js'
import Settings from './Settings'
import getReadme from '../getReadme'
import styles from './styles.module.css'
import * as format from '../format'
function Description({ repoName }) {
const isRelative = (src) => !src.match(/^(https?:|data:)/)
const urlTransform = (src) => {
if (isRelative(src)) return `http://raw.githubusercontent.com/${repoName}/master/${src}`
return src
}
const [readme, setReadme] = useState(null)
useEffect(() => { getReadme(repoName).then(setReadme) }, [])
if (!readme) return null
return (
<ReactMarkdown className={styles.markdown} transformImageUri={(src) => urlTransform(src)}>
{readme}
</ReactMarkdown>
)
}
Description.propTypes = {
repoName: PropTypes.string.isRequired
}
function Preview({ onComplete, plugin }) {
const [runningAction, setRunningAction] = useState(null)
const [showDescription, setShowDescription] = useState(null)
const [showSettings, setShowSettings] = useState(null)
const onCompleteAction = () => {
setRunningAction(null)
onComplete()
}
const pluginAction = (pluginName, runningActionName) => () => [
setRunningAction(runningActionName),
client[runningActionName](pluginName)
]
const {
name, version, description, repo,
isInstalled = false,
isDebugging = false,
installedVersion,
isUpdateAvailable = false
} = plugin
const githubRepo = repo && repo.match(/^.+github.com\/([^\/]+\/[^\/]+).*?/)
const settings = plugin?.settings || null
return (
<div className={styles.preview} key={name}>
<h2>{`${format.name(name)} (${version})`}</h2>
<p>{format.description(description)}</p>
<KeyboardNav>
<div className={styles.header}>
{settings && (
<KeyboardNavItem onSelect={() => setShowSettings((prev) => !prev)}>
Settings
</KeyboardNavItem>
)}
{showSettings && <Settings name={name} settings={settings} />}
{!isInstalled && !isDebugging && (
<ActionButton
action={pluginAction(name, 'install')}
text={runningAction === 'install' ? 'Installing...' : 'Install'}
onComplete={onCompleteAction}
/>
)}
{isInstalled && (
<ActionButton
action={pluginAction(name, 'uninstall')}
text={runningAction === 'uninstall' ? 'Uninstalling...' : 'Uninstall'}
onComplete={onCompleteAction}
/>
)}
{isUpdateAvailable && (
<ActionButton
action={pluginAction(name, 'update')}
text={runningAction === 'update' ? 'Updating...' : `Update (${installedVersion} → ${version})`}
onComplete={onCompleteAction}
/>
)}
{githubRepo && (
<KeyboardNavItem onSelect={() => setShowDescription((prev) => !prev)}>
Details
</KeyboardNavItem>
)}
</div>
</KeyboardNav>
{showDescription && <Description repoName={githubRepo[1]} />}
</div>
)
}
Preview.propTypes = {
plugin: PropTypes.object.isRequired,
onComplete: PropTypes.func.isRequired,
}
export default Preview
================================================
FILE: app/plugins/core/plugins/Preview/styles.module.css
================================================
.preview {
align-self: flex-start;
width: 100%;
}
.header {
border-bottom: var(--main-border);
margin-bottom: 15px;
}
.markdown {
font-size: .8em;
align-self: flex-start;
font-size: 16px;
padding: 0 10px;
p {
font-size: 1em;
margin: 0 0 10px;
}
h1, h2, h3, h4 {
color: var(--main-font-color);
margin-top: 24px;
margin-bottom: 16px;
font-weight: 500;
line-height: 1.25;
}
h1 {
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: var(--main-border);
}
h2 {
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: var(--main-border);
}
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
filter: invert(10%);
background: var(--main-background-color);
border-radius: 3px;
code {
padding: 0;
background: transparent;
&:before, &:after {
content: none;
}
}
}
blockquote {
border-left: 3px solid #999;
margin: 15px 0;
padding: 5px 15px;
p:last-child {
margin-bottom: 0;
}
}
code {
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
margin: 0;
font-size: 85%;
background-color: rgba(0,0,0,0.04);
border-radius: 3px;
&:after {
letter-spacing: -0.2em;
content: "\00a0";
}
&:before {
letter-spacing: -0.2em;
content: "\00a0";
}
}
img {
max-width: 100%;
}
a {
color: #4078c0;
text-decoration: none;
}
ul {
padding-left: 2em;
list-style-type: disc;
}
li {
list-style-type: disc;
}
li + li {
margin-top: 0.25em;
}
}
.settingsWrapper {
margin: 15px 0;
}
================================================
FILE: app/plugins/core/plugins/StatusBar/index.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import styles from './styles.module.css'
function StatusBar({ value }) {
return <div className={styles.statusBar}>{value}</div>
}
StatusBar.propTypes = {
value: PropTypes.string.isRequired
}
export default StatusBar
================================================
FILE: app/plugins/core/plugins/StatusBar/styles.module.css
================================================
.statusBar {
position: absolute;
right: 0;
bottom: 0;
z-index: 11;
font-size: 11px;
color: var(--secondary-font-color);
background: var(--preview-input-background);
padding: 5px;
border-radius: 5px 0 0 0;
border-top: var(--main-border);
border-left: var(--main-border);
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
================================================
FILE: app/plugins/core/plugins/blacklist.js
================================================
/**
* This file contains plugins that have been blacklisted.
* The main purpose of this is to hide plugins that have been republished under our scope.
* The name must match (case sensitive) the name in the `package.json`.
*/
export default [
'cerebro-basic-apps', // @cerebroapp/cerebro-basic-apps
'cerebro-mac-apps', // @cerebroapp/cerebro-mac-apps
'cerebro-brew', // @cerebroapp/cerebro-brew
]
================================================
FILE: app/plugins/core/plugins/format.js
================================================
import {
flow, words, capitalize, trim, map, join
} from 'lodash/fp'
/**
* Remove unnecessary information from plugin description
* like `Cerebro plugin for`
* @param {String} str
* @return {String}
*/
const removeDescriptionNoise = (str) => (
(str || '').replace(/^cerebro\s?(plugin)?\s?(to|for)?/i, '')
)
/**
* Remove unnecessary information from plugin name
* like `cerebro-plugin-` or `cerebro-`
* @param {String} str
* @return {String}
*/
const removeNameNoise = (str) => (
(str || '').replace(/^cerebro-(plugin)?-?/i, '')
)
export const name = (text = '') => flow(
trim,
words,
map(capitalize),
join(' ')
)(removeNameNoise(text.toLowerCase()))
export const description = flow(
removeDescriptionNoise,
trim,
capitalize,
)
export const version = (plugin) => (
plugin.isUpdateAvailable
? `${plugin.installedVersion} → ${plugin.version}`
: plugin.version
)
================================================
FILE: app/plugins/core/plugins/getAvailablePlugins.js
================================================
/**
* API endpoint to search all cerebro plugins
* @type {String}
*/
const URL = 'https://registry.npmjs.com/-/v1/search?from=0&size=500&text=keywords:cerebro-plugin,cerebro-extracted-plugin'
const sortByPopularity = (a, b) => a.score.detail.popularity > b.score.detail.popularity ? -1 : 1
/**
* Get all available plugins for Cerebro
* @return {Promise<Array>}
*/
export default async () => {
if (!navigator.onLine) return []
try {
const { objects: plugins } = await fetch(URL).then((res) => res.json())
plugins.sort(sortByPopularity)
return plugins.map((p) => ({
name: p.package.name,
version: p.package.version,
description: p.package.description,
homepage: p.package.links.homepage,
repo: p.package.links.repository
}))
} catch (err) {
console.log(err)
return []
}
}
================================================
FILE: app/plugins/core/plugins/getDebuggingPlugins.js
================================================
import path from 'path'
import { modulesDirectory } from 'lib/plugins'
import { lstatSync, readdirSync } from 'fs'
const isSymlink = (file) => lstatSync(path.join(modulesDirectory, file)).isSymbolicLink()
const isScopeDir = (file) => file.match(/^@/) && lstatSync(path.join(modulesDirectory, file)).isDirectory()
const getSymlinkedPluginsInFolder = (scope) => {
const files = scope
? readdirSync(path.join(modulesDirectory, scope))
: readdirSync(modulesDirectory)
return files.filter((name) => isSymlink(scope ? path.join(scope, name) : name))
}
const getNotScopedPluginNames = async () => getSymlinkedPluginsInFolder()
const getScopedPluginNames = async () => {
// Get all scoped folders
const scopeSubfolders = readdirSync(modulesDirectory).filter(isScopeDir)
// for each scope, get all plugins
const scopeNames = scopeSubfolders.map((scope) => {
const scopePlugins = getSymlinkedPluginsInFolder(scope)
return scopePlugins.map((plugin) => `${scope}/${plugin}`)
}).flat() // flatten array of arrays
return scopeNames
}
/**
* Get list of all plugins that are currently in debugging mode.
* These plugins are symlinked by [create-cerebro-plugin](https://github.com/cerebroapp/create-cerebro-plugin)
*
* @return {Promise<string[]>}
*/
export default async () => {
const [notScoppedPluginNames, scopedPluginNames] = await Promise.all([
getNotScopedPluginNames(),
getScopedPluginNames()
])
return [...notScoppedPluginNames, ...scopedPluginNames]
}
================================================
FILE: app/plugins/core/plugins/getInstalledPlugins.js
================================================
import { packageJsonPath } from 'lib/plugins'
import { readFile } from 'fs/promises'
import externalPlugins from 'plugins/externalPlugins'
const readPackageJson = async () => {
try {
const fileContent = await readFile(packageJsonPath, { encoding: 'utf8' })
return JSON.parse(fileContent)
} catch (err) {
console.log(err)
return {}
}
}
/**
* Get list of all installed plugins with versions
*
* @return {Promise<{[name: string]: Record<string, any>}>}
*/
export default async () => {
const packageJson = await readPackageJson()
const result = {}
Object.keys(externalPlugins).forEach((pluginName) => {
result[pluginName] = { ...externalPlugins[pluginName], version: packageJson.dependencies[pluginName] || '0.0.0' }
})
return result
}
================================================
FILE: app/plugins/core/plugins/getReadme.js
================================================
/**
* Get plugin Readme.md content
*
* @param {String} repository Repository field from npm package
* @return {Promise}
*/
export default (repo) => (
fetch(`https://api.github.com/repos/${repo}/readme`)
.then((response) => response.json())
.then((json) => Buffer.from(json.content, 'base64').toString())
)
================================================
FILE: app/plugins/core/plugins/index.js
================================================
import React from 'react'
import { search } from 'cerebro-tools'
import { shell } from 'electron'
import { partition } from 'lodash'
import {
flow, map, partialRight, tap
} from 'lodash/fp'
import store from 'main/store'
import * as statusBar from 'main/actions/statusBar'
import loadPlugins from './loadPlugins'
import icon from '../icon.png'
import * as format from './format'
import Preview from './Preview'
import initializeAsync from './initializeAsync'
const toString = ({ name, description }) => [name, description].join(' ')
const categories = [
['Development', (plugin) => plugin.isDebugging],
['Updates', (plugin) => plugin.isUpdateAvailable],
['Installed', (plugin) => plugin.isInstalled],
['Available', (plugin) => plugin.name],
]
const updatePlugin = async (update, name) => {
const plugins = await loadPlugins()
const updatedPlugin = plugins.find((plugin) => plugin.name === name)
update(name, {
title: `${format.name(updatedPlugin.name)} (${format.version(updatedPlugin)})`,
getPreview: () => (
<Preview
plugin={updatedPlugin}
key={name}
onComplete={() => updatePlugin(update, name)}
/>
)
})
}
const pluginToResult = (update) => (plugin) => {
if (typeof plugin === 'string') {
return { title: plugin }
}
return {
icon,
id: plugin.name,
title: `${format.name(plugin.name)} (${format.version(plugin)})`,
subtitle: format.description(plugin.description || ''),
onSelect: () => shell.openExternal(plugin.repo),
getPreview: () => (
<Preview
plugin={plugin}
key={plugin.name}
onComplete={() => updatePlugin(update, plugin.name)}
/>
)
}
}
const categorize = (plugins, callback) => {
const result = []
let remainder = plugins
categories.forEach((category) => {
const [title, filter] = category
const [matched, others] = partition(remainder, filter)
if (matched.length) result.push(title, ...matched)
remainder = others
})
plugins.splice(0, plugins.length)
plugins.push(...result)
callback()
}
const fn = ({
term, display, hide, update
}) => {
const match = term.match(/^plugins?\s*(.+)?$/i)
if (match) {
display({
icon,
id: 'loading',
title: 'Looking for plugins...'
})
loadPlugins().then(flow(
partialRight(search, [match[1], toString]),
tap((plugins) => categorize(plugins, () => hide('loading'))),
map(pluginToResult(update)),
display
))
}
}
const setStatusBar = (text) => {
store.dispatch(statusBar.setValue(text))
}
export default {
icon,
fn,
initializeAsync,
name: 'Manage plugins',
keyword: 'plugins',
onMessage: (type) => {
if (type === 'plugins:start-installation') {
setStatusBar('Installing default plugins...')
}
if (type === 'plugins:finish-installation') {
setTimeout(() => {
setStatusBar(null)
}, 2000)
}
}
}
================================================
FILE: app/plugins/core/plugins/initializeAsync.js
================================================
import { client } from 'lib/plugins'
import config from 'lib/config'
import {
flow, filter, map, property
} from 'lodash/fp'
import loadPlugins from './loadPlugins'
import getInstalledPlugins from './getInstalledPlugins'
const OS_APPS_PLUGIN = {
darwin: '@cerebroapp/cerebro-mac-apps',
DEFAULT: '@cerebroapp/cerebro-basic-apps'
}
const DEFAULT_PLUGINS = [
OS_APPS_PLUGIN[process.platform] || OS_APPS_PLUGIN.DEFAULT,
'@cerebroapp/search',
'cerebro-math',
'cerebro-converter',
'cerebro-open-web',
'cerebro-files-nav'
]
/**
* Check plugins for updates and start plugins autoupdater
*/
async function checkForUpdates() {
console.log('Run plugins autoupdate')
const plugins = await loadPlugins()
const updatePromises = flow(
filter(property('isUpdateAvailable')),
map((plugin) => client.update(plugin.name))
)(plugins)
await Promise.all(updatePromises)
console.log(updatePromises.length > 0
? `${updatePromises.length} plugins are updated`
: 'All plugins are up to date')
// Run autoupdate every 12 hours
setTimeout(checkForUpdates, 12 * 60 * 60 * 1000)
}
/**
* Migrate plugins: default plugins were extracted to separate packages
* so if default plugins are not installed – start installation
*/
async function migratePlugins(sendMessage) {
if (config.get('isMigratedPlugins')) {
// Plugins are already migrated
return
}
console.log('Start installation of default plugins')
const installedPlugins = await getInstalledPlugins()
const promises = flow(
filter((plugin) => !installedPlugins[plugin]),
map((plugin) => client.install(plugin))
)(DEFAULT_PLUGINS)
if (promises.length > 0) {
sendMessage('plugins:start-installation')
}
Promise.all(promises).then(() => {
console.log('All default plugins are installed!')
config.set('isMigratedPlugins', true)
sendMessage('plugins:finish-installation')
})
}
export default (sendMessage) => {
checkForUpdates()
migratePlugins(sendMessage)
}
================================================
FILE: app/plugins/core/plugins/loadPlugins.js
================================================
import { memoize } from 'cerebro-tools'
import validVersion from 'semver/functions/valid'
import compareVersions from 'semver/functions/gt'
import availablePlugins from './getAvailablePlugins'
import getInstalledPlugins from './getInstalledPlugins'
import getDebuggingPlugins from './getDebuggingPlugins'
import blacklist from './blacklist'
const maxAge = 5 * 60 * 1000 // 5 minutes
const getAvailablePlugins = memoize(availablePlugins, { maxAge })
const parseVersion = (version) => (
validVersion((version || '').replace(/^\^/, '')) || '0.0.0'
)
export default async () => {
const [available, installed, debuggingPlugins] = await Promise.all([
getAvailablePlugins(),
getInstalledPlugins(),
getDebuggingPlugins()
])
const listOfInstalledPlugins = Object.entries(installed).map(([name, { version }]) => ({
name,
version,
installedVersion: parseVersion(version),
isInstalled: true,
settings: installed[name].settings,
isUpdateAvailable: false
}))
const listOfAvailablePlugins = available.map((plugin) => {
const installedVersion = installed[plugin.name]?.version
if (!installedVersion) { return plugin }
const isUpdateAvailable = compareVersions(plugin.version, parseVersion(installedVersion))
const installedPluginInfo = listOfInstalledPlugins.find((p) => p.name === plugin.name)
return {
...plugin,
...installedPluginInfo,
installedVersion,
isInstalled: true,
isUpdateAvailable
}
})
console.log('Debugging Plugins: ', debuggingPlugins)
const listOfDebuggingPlugins = debuggingPlugins.map((name) => ({
name,
description: '',
version: 'dev',
isDebugging: true
}))
const plugins = [
...listOfInstalledPlugins,
...listOfAvailablePlugins,
...listOfDebuggingPlugins
].filter((plugin) => !blacklist.includes(plugin.name))
return plugins
}
================================================
FILE: app/plugins/core/quit/index.js
================================================
import { ipcRenderer } from 'electron'
import { search } from 'cerebro-tools'
import icon from '../icon.png'
const KEYWORDS = ['Quit', 'Exit']
const subtitle = 'Quit from Cerebro'
const onSelect = () => ipcRenderer.send('quit')
/**
* Plugin to exit from Cerebro
*
* @param {String} options.term
* @param {Function} options.display
*/
const fn = ({ term, display }) => {
const result = search(KEYWORDS, term).map((title) => ({
icon,
title,
subtitle,
onSelect,
term: title,
}))
display(result)
}
export default { fn }
================================================
FILE: app/plugins/core/reload/index.js
================================================
import { ipcRenderer } from 'electron'
import icon from '../icon.png'
const keyword = 'reload'
const title = 'Reload'
const subtitle = 'Reload Cerebro App'
const onSelect = (event) => {
ipcRenderer.send('reload')
event.preventDefault()
}
/**
* Plugin to reload Cerebro
*
* @param {String} options.term
* @param {Function} options.display
*/
const fn = ({ term, display }) => {
const match = term.match(/^reload\s*/)
if (match) {
display({
icon, title, subtitle, onSelect
})
}
}
export default {
keyword, fn, icon, name: 'Reload'
}
================================================
FILE: app/plugins/core/settings/Settings/Hotkey.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import styles from './styles.module.css'
const ASCII = {
188: '44',
109: '45',
190: '46',
191: '47',
192: '96',
220: '92',
222: '39',
221: '93',
219: '91',
173: '45',
187: '61',
186: '59',
189: '45'
}
const SHIFT_UPS = {
96: '~',
49: '!',
50: '@',
51: '#',
52: '$',
53: '%',
54: '^',
55: '&',
56: '*',
57: '(',
48: ')',
45: '_',
61: '+',
91: '{',
93: '}',
92: '|',
59: ':',
39: '"',
44: '<',
46: '>',
47: '?'
}
const KEYCODES = {
8: 'Backspace',
9: 'Tab',
13: 'Enter',
27: 'Esc',
32: 'Space',
37: 'Left',
38: 'Up',
39: 'Right',
40: 'Down',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
}
const osKeyDelimiter = process.platform === 'darwin' ? '' : '+'
const keyToSign = (key) => {
if (process.platform === 'darwin') {
return key.replace(/control/i, '⌃')
.replace(/alt/i, '⌥')
.replace(/shift/i, '⇧')
.replace(/command/i, '⌘')
.replace(/enter/i, '↩')
.replace(/backspace/i, '⌫')
}
return key
}
const charCodeToSign = ({ keyCode, shiftKey }) => {
if (KEYCODES[keyCode]) {
return KEYCODES[keyCode]
}
const valid = (keyCode > 47 && keyCode < 58) // number keys
|| (keyCode > 64 && keyCode < 91) // letter keys
|| (keyCode > 95 && keyCode < 112) // numpad keys
|| (keyCode > 185 && keyCode < 193) // ;=,-./` (in order)
|| (keyCode > 218 && keyCode < 223) // [\]' (in order)
if (!valid) {
return null
}
const code = ASCII[keyCode] ? ASCII[keyCode] : keyCode
if (!shiftKey && (code >= 65 && code <= 90)) {
return String.fromCharCode(code + 32)
}
if (shiftKey && SHIFT_UPS[code]) {
return SHIFT_UPS[code]
}
return String.fromCharCode(code)
}
function Hotkey({ hotkey, onChange }) {
const onKeyDown = (event) => {
if (!event.ctrlKey && !event.altKey && !event.metaKey) {
// Do not allow to set global shorcut without modifier keys
// At least one of alt, cmd or ctrl is required
return
}
event.preventDefault()
event.stopPropagation()
const key = charCodeToSign(event)
if (!key) return
const keys = []
if (event.ctrlKey) keys.push('Control')
if (event.altKey) keys.push('Alt')
if (event.shiftKey) keys.push('Shift')
if (event.metaKey) keys.push('Command')
keys.push(key)
onChange(keys.join('+'))
}
const keys = hotkey.split('+').map(keyToSign).join(osKeyDelimiter)
return (
<div>
<input
readOnly
className={styles.input}
type="text"
value={keys}
onKeyDown={onKeyDown}
/>
</div>
)
}
Hotkey.propTypes = {
hotkey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
export default Hotkey
================================================
FILE: app/plugins/core/settings/Settings/countries.js
================================================
export default [
{ value: 'AF', label: 'Afghanistan' },
{ value: 'AX', label: 'Åland Islands' },
{ value: 'AL', label: 'Albania' },
{ value: 'DZ', label: 'Algeria' },
{ value: 'AS', label: 'American Samoa' },
{ value: 'AD', label: 'Andorra' },
{ value: 'AO', label: 'Angola' },
{ value: 'AI', label: 'Anguilla' },
{ value: 'AQ', label: 'Antarctica' },
{ value: 'AG', label: 'Antigua and Barbuda' },
{ value: 'AR', label: 'Argentina' },
{ value: 'AM', label: 'Armenia' },
{ value: 'AW', label: 'Aruba' },
{ value: 'AU', label: 'Australia' },
{ value: 'AT', label: 'Austria' },
{ value: 'AZ', label: 'Azerbaijan' },
{ value: 'BS', label: 'The Bahamas' },
{ value: 'BH', label: 'Bahrain' },
{ value: 'BD', label: 'Bangladesh' },
{ value: 'BB', label: 'Barbados' },
{ value: 'BY', label: 'Belarus' },
{ value: 'BE', label: 'Belgium' },
{ value: 'BZ', label: 'Belize' },
{ value: 'BJ', label: 'Benin' },
{ value: 'BM', label: 'Bermuda' },
{ value: 'BT', label: 'Bhutan' },
{ value: 'BO', label: 'Bolivia' },
{ value: 'BQ', label: 'Bonaire' },
{ value: 'BA', label: 'Bosnia and Herzegovina' },
{ value: 'BW', label: 'Botswana' },
{ value: 'BV', label: 'Bouvet Island' },
{ value: 'BR', label: 'Brazil' },
{ value: 'IO', label: 'British Indian Ocean Territory' },
{ value: 'UM', label: 'United States Minor Outlying Islands' },
{ value: 'VG', label: 'Virgin Islands (British)' },
{ value: 'VI', label: 'Virgin Islands (U.S.)' },
{ value: 'BN', label: 'Brunei' },
{ value: 'BG', label: 'Bulgaria' },
{ value: 'BF', label: 'Burkina Faso' },
{ value: 'BI', label: 'Burundi' },
{ value: 'KH', label: 'Cambodia' },
{ value: 'CM', label: 'Cameroon' },
{ value: 'CA', label: 'Canada' },
{ value: 'CV', label: 'Cape Verde' },
{ value: 'KY', label: 'Cayman Islands' },
{ value: 'CF', label: 'Central African Republic' },
{ value: 'TD', label: 'Chad' },
{ value: 'CL', label: 'Chile' },
{ value: 'CN', label: 'China' },
{ value: 'CX', label: 'Christmas Island' },
{ value: 'CC', label: 'Cocos (Keeling) Islands' },
{ value: 'CO', label: 'Colombia' },
{ value: 'KM', label: 'Comoros' },
{ value: 'CG', label: 'Republic of the Congo' },
{ value: 'CD', label: 'Democratic Republic of the Congo' },
{ value: 'CK', label: 'Cook Islands' },
{ value: 'CR', label: 'Costa Rica' },
{ value: 'HR', label: 'Croatia' },
{ value: 'CU', label: 'Cuba' },
{ value: 'CW', label: 'Curaçao' },
{ value: 'CY', label: 'Cyprus' },
{ value: 'CZ', label: 'Czech Republic' },
{ value: 'DK', label: 'Denmark' },
{ value: 'DJ', label: 'Djibouti' },
{ value: 'DM', label: 'Dominica' },
{ value: 'DO', label: 'Dominican Republic' },
{ value: 'EC', label: 'Ecuador' },
{ value: 'EG', label: 'Egypt' },
{ value: 'SV', label: 'El Salvador' },
{ value: 'GQ', label: 'Equatorial Guinea' },
{ value: 'ER', label: 'Eritrea' },
{ value: 'EE', label: 'Estonia' },
{ value: 'ET', label: 'Ethiopia' },
{ value: 'FK', label: 'Falkland Islands' },
{ value: 'FO', label: 'Faroe Islands' },
{ value: 'FJ', label: 'Fiji' },
{ value: 'FI', label: 'Finland' },
{ value: 'FR', label: 'France' },
{ value: 'GF', label: 'French Guiana' },
{ value: 'PF', label: 'French Polynesia' },
{ value: 'TF', label: 'French Southern and Antarctic Lands' },
{ value: 'GA', label: 'Gabon' },
{ value: 'GM', label: 'The Gambia' },
{ value: 'GE', label: 'Georgia' },
{ value: 'DE', label: 'Germany' },
{ value: 'GH', label: 'Ghana' },
{ value: 'GI', label: 'Gibraltar' },
{ value: 'GR', label: 'Greece' },
{ value: 'GL', label: 'Greenland' },
{ value: 'GD', label: 'Grenada' },
{ value: 'GP', label: 'Guadeloupe' },
{ value: 'GU', label: 'Guam' },
{ value: 'GT', label: 'Guatemala' },
{ value: 'GG', label: 'Guernsey' },
{ value: 'GW', label: 'Guinea-Bissau' },
{ value: 'GY', label: 'Guyana' },
{ value: 'HT', label: 'Haiti' },
{ value: 'HM', label: 'Heard Island and McDonald Islands' },
{ value: 'VA', label: 'Holy See' },
{ value: 'HN', label: 'Honduras' },
{ value: 'HK', label: 'Hong Kong' },
{ value: 'HU', label: 'Hungary' },
{ value: 'IS', label: 'Iceland' },
{ value: 'IN', label: 'India' },
{ value: 'ID', label: 'Indonesia' },
{ value: 'CI', label: 'Ivory Coast' },
{ value: 'IR', label: 'Iran' },
{ value: 'IQ', label: 'Iraq' },
{ value: 'IE', label: 'Republic of Ireland' },
{ value: 'IM', label: 'Isle of Man' },
{ value: 'IL', label: 'Israel' },
{ value: 'IT', label: 'Italy' },
{ value: 'JM', label: 'Jamaica' },
{ value: 'JP', label: 'Japan' },
{ value: 'JE', label: 'Jersey' },
{ value: 'JO', label: 'Jordan' },
{ value: 'KZ', label: 'Kazakhstan' },
{ value: 'KE', label: 'Kenya' },
{ value: 'KI', label: 'Kiribati' },
{ value: 'KW', label: 'Kuwait' },
{ value: 'KG', label: 'Kyrgyzstan' },
{ value: 'LA', label: 'Laos' },
{ value: 'LV', label: 'Latvia' },
{ value: 'LB', label: 'Lebanon' },
{ value: 'LS', label: 'Lesotho' },
{ value: 'LR', label: 'Liberia' },
{ value: 'LY', label: 'Libya' },
{ value: 'LI', label: 'Liechtenstein' },
{ value: 'LT', label: 'Lithuania' },
{ value: 'LU', label: 'Luxembourg' },
{ value: 'MO', label: 'Macau' },
{ value: 'MK', label: 'Republic of Macedonia' },
{ value: 'MG', label: 'Madagascar' },
{ value: 'MW', label: 'Malawi' },
{ value: 'MY', label: 'Malaysia' },
{ value: 'MV', label: 'Maldives' },
{ value: 'ML', label: 'Mali' },
{ value: 'MT', label: 'Malta' },
{ value: 'MH', label: 'Marshall Islands' },
{ value: 'MQ', label: 'Martinique' },
{ value: 'MR', label: 'Mauritania' },
{ value: 'MU', label: 'Mauritius' },
{ value: 'YT', label: 'Mayotte' },
{ value: 'MX', label: 'Mexico' },
{ value: 'FM', label: 'Federated States of Micronesia' },
{ value: 'MD', label: 'Moldova' },
{ value: 'MC', label: 'Monaco' },
{ value: 'MN', label: 'Mongolia' },
{ value: 'ME', label: 'Montenegro' },
{ value: 'MS', label: 'Montserrat' },
{ value: 'MA', label: 'Morocco' },
{ value: 'MZ', label: 'Mozambique' },
{ value: 'MM', label: 'Myanmar' },
{ value: 'NA', label: 'Namibia' },
{ value: 'NR', label: 'Nauru' },
{ value: 'NP', label: 'Nepal' },
{ value: 'NL', label: 'Netherlands' },
{ value: 'NC', label: 'New Caledonia' },
{ value: 'NZ', label: 'New Zealand' },
{ value: 'NI', label: 'Nicaragua' },
{ value: 'NE', label: 'Niger' },
{ value: 'NG', label: 'Nigeria' },
{ value: 'NU', label: 'Niue' },
{ value: 'NF', label: 'Norfolk Island' },
{ value: 'KP', label: 'North Korea' },
{ value: 'MP', label: 'Northern Mariana Islands' },
{ value: 'NO', label: 'Norway' },
{ value: 'OM', label: 'Oman' },
{ value: 'PK', label: 'Pakistan' },
{ value: 'PW', label: 'Palau' },
{ value: 'PS', label: 'Palestine' },
{ value: 'PA', label: 'Panama' },
{ value: 'PG', label: 'Papua New Guinea' },
{ value: 'PY', label: 'Paraguay' },
{ value: 'PE', label: 'Peru' },
{ value: 'PH', label: 'Philippines' },
{ value: 'PN', label: 'Pitcairn Islands' },
{ value: 'PL', label: 'Poland' },
{ value: 'PT', label: 'Portugal' },
{ value: 'PR', label: 'Puerto Rico' },
{ value: 'QA', label: 'Qatar' },
{ value: 'XK', label: 'Republic of Kosovo' },
{ value: 'RE', label: 'Réunion' },
{ value: 'RO', label: 'Romania' },
{ value: 'RU', label: 'Russia' },
{ value: 'RW', label: 'Rwanda' },
{ value: 'BL', label: 'Saint Barthélemy' },
{ value: 'SH', label: 'Saint Helena' },
{ value: 'KN', label: 'Saint Kitts and Nevis' },
{ value: 'LC', label: 'Saint Lucia' },
{ value: 'MF', label: 'Saint Martin' },
{ value: 'PM', label: 'Saint Pierre and Miquelon' },
{ value: 'VC', label: 'Saint Vincent and the Grenadines' },
{ value: 'WS', label: 'Samoa' },
{ value: 'SM', label: 'San Marino' },
{ value: 'ST', label: 'São Tomé and Príncipe' },
{ value: 'SA', label: 'Saudi Arabia' },
{ value: 'SN', label: 'Senegal' },
{ value: 'RS', label: 'Serbia' },
{ value: 'SC', label: 'Seychelles' },
{ value: 'SL', label: 'Sierra Leone' },
{ value: 'SG', label: 'Singapore' },
{ value: 'SX', label: 'Sint Maarten' },
{ value: 'SK', label: 'Slovakia' },
{ value: 'SI', label: 'Slovenia' },
{ value: 'SB', label: 'Solomon Islands' },
{ value: 'SO', label: 'Somalia' },
{ value: 'ZA', label: 'South Africa' },
{ value: 'GS', label: 'South Georgia' },
{ value: 'KR', label: 'South Korea' },
{ value: 'SS', label: 'South Sudan' },
{ value: 'ES', label: 'Spain' },
{ value: 'LK', label: 'Sri Lanka' },
{ value: 'SD', label: 'Sudan' },
{ value: 'SR', label: 'Surinae' },
{ value: 'SJ', label: 'Svalbard and Jan Mayen' },
{ value: 'SZ', label: 'Swaziland' },
{ value: 'SE', label: 'Sweden' },
{ value: 'CH', label: 'Switzerland' },
{ value: 'SY', label: 'Syria' },
{ value: 'TW', label: 'Taiwan' },
{ value: 'TJ', label: 'Tajikistan' },
{ value: 'TZ', label: 'Tanzania' },
{ value: 'TH', label: 'Thailand' },
{ value: 'TL', label: 'East Timor' },
{ value: 'TG', label: 'Togo' },
{ value: 'TK', label: 'Tokelau' },
{ value: 'TO', label: 'Tonga' },
{ value: 'TT', label: 'Trinidad and Tobago' },
{ value: 'TN', label: 'Tunisia' },
{ value: 'TR', label: 'Turkey' },
{ value: 'TM', label: 'Turkmenistan' },
{ value: 'TC', label: 'Turks and Caicos Islands' },
{ value: 'TV', label: 'Tuvalu' },
{ value: 'UG', label: 'Uganda' },
{ value: 'UA', label: 'Ukraine' },
{ value: 'AE', label: 'United Arab Emirates' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'US', label: 'United States' },
{ value: 'UY', label: 'Uruguay' },
{ value: 'UZ', label: 'Uzbekistan' },
{ value: 'VU', label: 'Vanuatu' },
{ value: 'VE', label: 'Venezuela' },
{ value: 'VN', label: 'Vietnam' },
{ value: 'WF', label: 'Wallis and Futuna' },
{ value: 'EH', label: 'Western Sahara' },
{ value: 'YE', label: 'Yemen' },
{ value: 'ZM', label: 'Zambia' },
{ value: 'ZW', label: 'Zimbabwe' }
]
================================================
FILE: app/plugins/core/settings/Settings/index.js
================================================
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { FormComponents } from '@cerebroapp/cerebro-ui'
import themes from 'lib/themes'
import Hotkey from './Hotkey'
import countries from './countries'
import styles from './styles.module.css'
const {
Select, Checkbox, Wrapper, Text
} = FormComponents
function Settings({ get, set }) {
const [state, setState] = useState(() => ({
hotkey: get('hotkey'),
showInTray: get('showInTray'),
country: get('country'),
theme: get('theme'),
proxy: get('proxy'),
developerMode: get('developerMode'),
cleanOnHide: get('cleanOnHide'),
selectOnShow: get('selectOnShow'),
pluginsSettings: get('plugins'),
openAtLogin: get('openAtLogin'),
searchBarPlaceholder: get('searchBarPlaceholder')
}))
const changeConfig = (key, value) => {
set(key, value)
setState((prevState) => ({ ...prevState, [key]: value }))
}
return (
<div className={styles.settings}>
<Wrapper label="Hotkey" description="Type your global shortcut for Cerebro in this input">
<Hotkey
hotkey={state.hotkey}
onChange={(key) => changeConfig('hotkey', key)}
/>
</Wrapper>
<Select
label="Country"
description="Choose your country so Cerebro can better choose currency, language, etc."
value={countries.find((c) => c.value === state.country)}
options={countries}
onChange={(value) => changeConfig('country', value)}
/>
<Select
label="Theme"
value={themes.find((t) => t.value === state.theme)}
options={themes}
onChange={(value) => changeConfig('theme', value)}
/>
<Text
type="text"
label="Proxy"
value={state.proxy}
onChange={(value) => changeConfig('proxy', value)}
/>
<Text
type="text"
label="Search bar placeholder"
value={state.searchBarPlaceholder}
onChange={(value) => changeConfig('searchBarPlaceholder', value)}
/>
<Checkbox
label="Open at login"
value={state.openAtLogin}
onChange={(value) => changeConfig('openAtLogin', value)}
/>
<Checkbox
label="Show in menu bar"
value={state.showInTray}
onChange={(value) => changeConfig('showInTray', value)}
/>
<Checkbox
label="Developer Mode"
value={state.developerMode}
onChange={(value) => changeConfig('developerMode', value)}
/>
<Checkbox
label="Clean results on hide"
value={state.cleanOnHide}
onChange={(value) => changeConfig('cleanOnHide', value)}
/>
<Checkbox
label="Select input on show"
value={state.selectOnShow}
onChange={(value) => changeConfig('selectOnShow', value)}
/>
</div>
)
}
Settings.propTypes = {
get: PropTypes.func.isRequired,
set: PropTypes.func.isRequired
}
export default Settings
================================================
FILE: app/plugins/core/settings/Settings/styles.module.css
================================================
.settings {
display: flex;
align-self: flex-start;
flex-direction: column;
align-items: center;
}
.label {
margin-right: 15px;
margin-top: 8px;
min-width: 60px;
max-width: 60px;
}
.checkbox {
margin-right: 5px;
}
.settingItem {
padding: 20px;
box-sizing: border-box;
width: 100%;
border-color: #d9d9d9 #ccc #b3b3b3;
border-top: 1px solid #ccc;
margin-top: 16px;
}
.header {
font-weight: bold;
}
.input {
font-size: 16px;
line-height: 34px;
padding: 0 10px;
box-sizing: border-box;
width: 100%;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
}
================================================
FILE: app/plugins/core/settings/index.js
================================================
import React from 'react'
import { search } from 'cerebro-tools'
import Settings from './Settings'
import icon from '../icon.png'
// Settings plugin name
const NAME = 'Cerebro Settings'
// Settings plugins in the end of list
const order = 9
// Phrases that used to find settings plugins
const KEYWORDS = [
NAME,
'Cerebro Preferences',
'cfg',
'config',
'params'
]
/**
* Plugin to show app settings in results list
*
* @param {String} options.term
* @param {Function} options.display
*/
const settingsPlugin = ({
term, display, config, actions
}) => {
const found = search(KEYWORDS, term).length > 0
if (found) {
const results = [{
order,
icon,
title: NAME,
term: NAME,
getPreview: () => (
<Settings
set={(key, value) => config.set(key, value)}
get={(key) => config.get(key)}
/>
),
onSelect: (event) => {
event.preventDefault()
actions.replaceTerm(NAME)
}
}]
display(results)
}
}
export default {
name: NAME,
fn: settingsPlugin
}
================================================
FILE: app/plugins/core/version/index.js
================================================
import React from 'react'
import { search } from 'cerebro-tools'
import icon from '../icon.png'
// Settings plugin name
const NAME = 'Cerebro Version'
// Settings plugins in the end of list
const order = 9
// Phrases that used to find settings plugins
const KEYWORDS = [
NAME,
'ver',
'version'
]
const { CEREBRO_VERSION } = process.env
/**
* Plugin to show app settings in results list
*
* @param {String} options.term
* @param {Function} options.display
*/
const versionPlugin = ({ term, display, actions }) => {
const found = search(KEYWORDS, term).length > 0
if (found) {
const results = [{
order,
icon,
title: NAME,
term: NAME,
getPreview: () => (<div><strong>{CEREBRO_VERSION}</strong></div>),
onSelect: (event) => {
event.preventDefault()
actions.replaceTerm(NAME)
}
}]
display(results)
}
}
export default { name: NAME, fn: versionPlugin }
================================================
FILE: app/plugins/externalPlugins.js
================================================
import debounce from 'lodash/debounce'
import chokidar from 'chokidar'
import path from 'path'
import initPlugin from 'lib/initPlugin'
import { modulesDirectory, ensureFiles, settings } from 'lib/plugins'
const plugins = {}
const requirePlugin = (pluginPath) => {
try {
let plugin = window.require(pluginPath)
// Fallback for plugins with structure like `{default: {fn: ...}}`
const keys = Object.keys(plugin)
if (keys.length === 1 && keys[0] === 'default') {
plugin = plugin.default
}
return plugin
} catch (error) {
// catch all errors from plugin loading
console.log('Error requiring', pluginPath)
console.log(error)
}
}
/**
* Validate plugin module signature
*
* @param {Object} plugin
* @return {Boolean}
*/
const isPluginValid = (plugin) => (
plugin
// Check existing of main plugin function
&& typeof plugin.fn === 'function'
// Check that plugin function accepts 0 or 1 argument
&& plugin.fn.length <= 1
)
ensureFiles()
/* As we support scoped plugins, using 'base' as plugin name is no longer valid
because it is not unique. '@example/plugin' and '@test/plugin' would both be treated as 'plugin'
So now we must introduce the scope to the plugin name
This function returns the name with the scope if it is present in the path
*/
const getPluginName = (pluginPath) => {
const { base, dir } = path.parse(pluginPath)
const scope = dir.match(/@.+$/)
if (!scope) return base
return `${scope[0]}/${base}`
}
const setupPluginsWatcher = () => {
if (global.isBackground) return
const pluginsWatcher = chokidar.watch(modulesDirectory, { depth: 1 })
pluginsWatcher.on('unlinkDir', (pluginPath) => {
const { base, dir } = path.parse(pluginPath)
if (base.match(/node_modules/) || base.match(/^@/)) return
if (!dir.match(/node_modules$/) && !dir.match(/@.+$/)) return
const pluginName = getPluginName(pluginPath)
const requirePath = window.require.resolve(pluginPath)
delete plugins[pluginName]
delete window.require.cache[requirePath]
console.log(`[${pluginName}] Plugin removed`)
})
pluginsWatcher.on('addDir', (pluginPath) => {
const { base, dir } = path.parse(pluginPath)
if (base.match(/node_modules/) || base.match(/^@/)) return
if (!dir.match(/node_modules$/) && !dir.match(/@.+$/)) return
const pluginName = getPluginName(pluginPath)
setTimeout(() => {
console.group(`Load plugin: ${pluginName}`)
console.log(`Path: ${pluginPath}...`)
const plugin = requirePlugin(pluginPath)
if (!isPluginValid(plugin)) {
console.log('Plugin is not valid, skipped')
console.groupEnd()
return
}
if (!settings.validate(plugin)) {
console.log('Invalid plugins settings')
console.groupEnd()
return
}
console.log('Loaded.')
const requirePath = window.require.resolve(pluginPath)
const watcher = chokidar.watch(pluginPath, { depth: 0 })
watcher.on('change', debounce(() => {
console.log(`[${pluginName}] Update plugin`)
delete window.require.cache[requirePath]
plugins[pluginName] = window.require(pluginPath)
console.log(`[${pluginName}] Plugin updated`)
}, 1000))
plugins[pluginName] = plugin
initPlugin(plugin, pluginName)
console.groupEnd()
}, 1000)
})
}
setupPluginsWatcher()
export default plugins
================================================
FILE: app/plugins/index.ts
================================================
import core from './core'
import externalPlugins from './externalPlugins'
const pluginsService = {
corePlugins: core,
allPlugins: Object.assign(externalPlugins, core),
externalPlugins,
}
export default pluginsService
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env', {
/** Targets must match the versions supported by electron.
* See https://www.electronjs.org/
*/
targets: {
node: '16',
chrome: '102'
}
}
],
'@babel/preset-react'
]
}
================================================
FILE: build/installer.nsh
================================================
!macro customInstall
DetailPrint "Register cerebro URI Handler"
DeleteRegKey HKCR "cerebro"
WriteRegStr HKCR "cerebro" "" "URL:cerebro"
WriteRegStr HKCR "cerebro" "URL Protocol" ""
WriteRegStr HKCR "cerebro\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
WriteRegStr HKCR "cerebro\shell" "" ""
WriteRegStr HKCR "cerebro\shell\Open" "" ""
WriteRegStr HKCR "cerebro\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
!macroend
================================================
FILE: electron-builder.json
================================================
{
"productName": "Cerebro",
"appId": "com.cerebroapp.Cerebro",
"protocols": {
"name": "Cerebro URLs",
"role": "Viewer",
"schemes": [
"cerebro"
]
},
"directories": {
"app": "./app",
"output": "release"
},
"linux": {
"target": [
{
"target": "deb",
"arch": [
"x64"
]
},
{
"target": "AppImage",
"arch": [
"x64"
]
}
],
"category": "Utility"
},
"mac": {
"category": "public.app-category.productivity"
},
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
},
{
"target": "portable",
"arch": [
"x64",
"ia32"
]
}
]
},
"nsis": {
"include": "build/installer.nsh",
"perMachine": true
},
"files": [
"dist/",
"main/index.html",
"main/css,",
"background/index.html",
"tray_icon.png",
"tray_icon.ico",
"tray_iconTemplate@2x.png",
"node_modules/",
"app/node_modules/",
"main.js",
"main.js.map",
"package.json",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}",
"!**/node_modules/.bin",
"!**/*.{o,hprof,orig,pyc,pyo,rbc}",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.editorconfig,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}"
],
"squirrelWindows": {
"iconUrl": "https://raw.githubusercontent.com/cerebroapp/cerebro/master/build/icon.ico"
},
"publish": {
"provider": "github",
"vPrefixedTagName": true,
"releaseType": "release"
}
}
================================================
FILE: jest.config.js
================================================
module.exports = {
collectCoverage: true,
moduleDirectories: ['node_modules', 'app'],
moduleNameMapper: {
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
'\\.(css|less)$': '<rootDir>/__mocks__/fileMock.js'
}
}
================================================
FILE: package.json
================================================
{
"name": "cerebro",
"productName": "cerebro",
"version": "0.11.0",
"description": "Cerebro is an open-source launcher to improve your productivity and efficiency",
"main": "./app/main.js",
"scripts": {
"test": "cross-env NODE_ENV=test CEREBRO_DATA_PATH=userdata jest",
"test-watch": "jest -- --watch",
"lint": "eslint app/background app/lib app/main app/plugins *.js",
"hot-server": "node -r @babel/register server.js",
"build-main": "webpack --mode production --config webpack.config.electron.js",
"build-main-dev": "webpack --mode development --config webpack.config.electron.js",
"build-renderer": "webpack --config webpack.config.production.js",
"bundle-analyze": "cross-env ANALYZE=true node ./node_modules/webpack/bin/webpack --config webpack.config.production.js && open ./app/dist/stats.html",
"build": "run-p build-main build-renderer",
"start": "cross-env NODE_ENV=production electron ./app",
"start-hot": "yarn build-main-dev && cross-env NODE_ENV=development electron ./app",
"release": "build -mwl --draft",
"dev": "run-p hot-server start-hot",
"postinstall": "electron-builder install-app-deps",
"package": "yarn build && npx electron-builder",
"prepare": "husky install",
"commit": "cz"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerebroapp/cerebro.git"
},
"author": {
"name": "CerebroApp Organization",
"email": "kelionweb@gmail.com",
"url": "https://github.com/cerebroapp"
},
"contributors": [
"Alexandr Subbotin <kelionweb@gmail.com> (https://github.com/KELiON)",
"Gustavo Pereira <oguhpereira@protonmail.com (https://github.com/oguhpereira)"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/cerebroapp/cerebro/issues"
},
"keywords": [
"cerebro",
"cerebroapp",
"launcher",
"electron"
],
"homepage": "https://cerebroapp.com",
"devDependencies": {
"@babel/core": "7.20.12",
"@babel/eslint-parser": "7.19.1",
"@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@babel/register": "7.18.9",
"@commitlint/cli": "17.4.2",
"@commitlint/config-conventional": "17.4.2",
"autoprefixer": "10.4.16",
"babel-loader": "8.2.5",
"commitizen": "4.3.0",
"copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
"css-loader": "6.7.3",
"cz-conventional-changelog": "3.3.0",
"electron": "20.2.0",
"electron-builder": "23.6.0",
"eslint": "8.34.0",
"eslint-config-airbnb": "19.0.4",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jest": "26.9.0",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"express": "4.18.2",
"husky": "8.0.3",
"jest": "27.5.1",
"lodash-webpack-plugin": "0.11.6",
"mini-css-extract-plugin": "2.7.6",
"npm-run-all": "4.1.5",
"postcss": "8.4.32",
"postcss-loader": "7.0.2",
"postcss-nested": "6.0.1",
"style-loader": "3.3.3",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"url-loader": "4.1.1",
"webpack": "5.104.1",
"webpack-cli": "4.10.0",
"webpack-dev-middleware": "5.3.3",
"webpack-hot-middleware": "2.25.4",
"webpack-visualizer-plugin": "0.1.11"
},
"dependencies": {},
"devEngines": {
"node": ">=16.x"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
'postcss-nested': {},
autoprefixer: {}
}
}
================================================
FILE: server.js
================================================
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const config = require('./webpack.config.development')
const app = express()
const compiler = webpack(config)
const PORT = 3000
const wdm = webpackDevMiddleware(compiler)
app.use(wdm)
app.use(webpackHotMiddleware(compiler))
const server = app.listen(PORT, 'localhost', (err) => {
if (err) {
console.error(err)
return
}
console.log(`Listening at http://localhost:${PORT}`)
})
process.on('SIGTERM', () => {
console.log('Stopping dev server')
wdm.close()
server.close(() => {
process.exit(0)
})
})
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "app",
"jsx": "react",
"allowJs": true,
"noImplicitAny": true,
"sourceMap": true,
"esModuleInterop": true,
},
"include": ["./app"]
}
================================================
FILE: webpack.config.base.js
================================================
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
module.exports = {
module: {
rules: [{
test: /\.(js|ts)x?$/,
use: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$|\.wav$|\.mp3$/,
type: 'asset/inline'
}]
},
output: {
path: path.join(__dirname, 'app'),
filename: '[name].bundle.js',
libraryTarget: 'commonjs2'
},
resolve: {
modules: [
path.join(__dirname, 'app'),
'node_modules'
],
extensions: ['.ts', '.js', '.tsx', '.jsx'],
},
plugins: [
new LodashModuleReplacementPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: 'app/main/css/themes/*',
to: './main/css/themes/[name][ext]'
}]
})
]
}
================================================
FILE: webpack.config.development.js
================================================
const webpack = require('webpack')
const baseConfig = require('./webpack.config.base')
const config = {
...baseConfig,
mode: 'development',
devtool: 'inline-source-map',
entry: {
background: [
'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr',
'./app/background/background',
],
main: [
'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr',
'./app/main/main',
]
},
output: {
...baseConfig.output,
publicPath: 'http://localhost:3000/dist/'
},
module: {
...baseConfig.module,
rules: [
...baseConfig.module.rules,
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: true,
importLoaders: 1,
},
},
'postcss-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
exclude: /\.module\.css$/,
},
]
},
plugins: [
...baseConfig.plugins,
new webpack.LoaderOptionsPlugin({
debug: true
}),
new webpack.HotModuleReplacementPlugin(),
],
stats: {
colors: true,
},
target: 'electron-renderer'
}
module.exports = config
================================================
FILE: webpack.config.electron.js
================================================
const baseConfig = require('./webpack.config.base')
module.exports = {
...baseConfig,
module: {
rules: [{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: ['babel-loader']
}]
},
devtool: 'source-map',
entry: './app/main.development',
output: {
...baseConfig.output,
filename: './main.js'
},
target: 'electron-main'
}
================================================
FILE: webpack.config.production.js
================================================
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const Visualizer = require('webpack-visualizer-plugin')
const baseConfig = require('./webpack.config.base')
const config = {
...baseConfig,
mode: 'production',
devtool: 'source-map',
entry: {
main: './app/main/main',
background: './app/background/background'
},
output: {
...baseConfig.output,
path: path.join(__dirname, 'app', 'dist'),
publicPath: '../dist/'
},
module: {
...baseConfig.module,
rules: [
...baseConfig.module.rules,
{
test: /\.css$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: true,
importLoaders: 1,
},
},
'postcss-loader',
],
include: /\.module\.css$/,
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
exclude: /\.module\.css$/,
},
]
},
plugins: [
...baseConfig.plugins,
new MiniCssExtractPlugin()
],
target: 'electron-renderer'
}
if (process.env.ANALYZE) {
config.plugins.push(new Visualizer())
}
module.exports = config
gitextract_k8ttga3x/ ├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github/ │ ├── issue_template.md │ ├── pull_request_template.md │ └── workflows/ │ ├── build.yml │ └── pr.yml ├── .gitignore ├── .husky/ │ └── commit-msg ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── __mocks__/ │ ├── @electron/ │ │ └── remote.js │ ├── electron-store.js │ ├── electron.js │ ├── fileMock.js │ └── plugins.js ├── app/ │ ├── background/ │ │ ├── background.js │ │ ├── createWindow.js │ │ └── index.html │ ├── initAutoUpdater.js │ ├── lib/ │ │ ├── __tests__/ │ │ │ └── loadThemes.spec.js │ │ ├── config.js │ │ ├── initPlugin.js │ │ ├── initializePlugins.js │ │ ├── plugins/ │ │ │ ├── index.js │ │ │ ├── npm.js │ │ │ └── settings/ │ │ │ ├── __tests__/ │ │ │ │ ├── get.spec.js │ │ │ │ └── validate.spec.js │ │ │ ├── get.js │ │ │ ├── index.js │ │ │ └── validate.js │ │ ├── rpc.js │ │ └── themes.ts │ ├── main/ │ │ ├── actions/ │ │ │ ├── __tests__/ │ │ │ │ ├── search.spec.js │ │ │ │ └── statusBar.spec.js │ │ │ ├── search.js │ │ │ └── statusBar.ts │ │ ├── components/ │ │ │ ├── Cerebro/ │ │ │ │ ├── index.js │ │ │ │ └── styles.module.css │ │ │ ├── ResultsList/ │ │ │ │ ├── Row/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.css │ │ │ │ ├── index.js │ │ │ │ └── styles.module.css │ │ │ ├── SmartIcon/ │ │ │ │ ├── getFileIcon/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── mac.ts │ │ │ │ │ └── windows.ts │ │ │ │ └── index.tsx │ │ │ └── StatusBar/ │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── constants/ │ │ │ ├── actionTypes.ts │ │ │ └── ui.ts │ │ ├── createWindow/ │ │ │ ├── AppTray.js │ │ │ ├── autoStart.js │ │ │ ├── buildMenu.js │ │ │ ├── checkForUpdates.js │ │ │ ├── handleUrl.js │ │ │ ├── showWindowWithTerm.ts │ │ │ └── toggleWindow.ts │ │ ├── createWindow.js │ │ ├── css/ │ │ │ ├── global.css │ │ │ ├── system-font.css │ │ │ └── themes/ │ │ │ ├── dark.css │ │ │ └── light.css │ │ ├── index.html │ │ ├── main.js │ │ ├── reducers/ │ │ │ ├── index.js │ │ │ ├── search.js │ │ │ └── statusBar.js │ │ └── store/ │ │ ├── configureStore.js │ │ └── index.ts │ ├── main.development.js │ ├── package.json │ └── plugins/ │ ├── core/ │ │ ├── autocomplete/ │ │ │ └── index.js │ │ ├── index.ts │ │ ├── plugins/ │ │ │ ├── Preview/ │ │ │ │ ├── ActionButton.js │ │ │ │ ├── FormItem.js │ │ │ │ ├── Settings.js │ │ │ │ ├── index.js │ │ │ │ └── styles.module.css │ │ │ ├── StatusBar/ │ │ │ │ ├── index.js │ │ │ │ └── styles.module.css │ │ │ ├── blacklist.js │ │ │ ├── format.js │ │ │ ├── getAvailablePlugins.js │ │ │ ├── getDebuggingPlugins.js │ │ │ ├── getInstalledPlugins.js │ │ │ ├── getReadme.js │ │ │ ├── index.js │ │ │ ├── initializeAsync.js │ │ │ └── loadPlugins.js │ │ ├── quit/ │ │ │ └── index.js │ │ ├── reload/ │ │ │ └── index.js │ │ ├── settings/ │ │ │ ├── Settings/ │ │ │ │ ├── Hotkey.js │ │ │ │ ├── countries.js │ │ │ │ ├── index.js │ │ │ │ └── styles.module.css │ │ │ └── index.js │ │ └── version/ │ │ └── index.js │ ├── externalPlugins.js │ └── index.ts ├── babel.config.js ├── build/ │ ├── icon.icns │ └── installer.nsh ├── electron-builder.json ├── jest.config.js ├── package.json ├── postcss.config.js ├── server.js ├── tsconfig.json ├── webpack.config.base.js ├── webpack.config.development.js ├── webpack.config.electron.js └── webpack.config.production.js
SYMBOL INDEX (93 symbols across 35 files)
FILE: __mocks__/electron-store.js
class Store (line 1) | class Store {
method get (line 2) | get() {
method set (line 6) | set() {}
FILE: app/initAutoUpdater.js
constant TEN_SECONDS (line 5) | const TEN_SECONDS = 10 * 1000
constant ONE_HOUR (line 6) | const ONE_HOUR = 60 * 60 * 1000
FILE: app/lib/plugins/index.js
constant EMPTY_PACKAGE_JSON (line 17) | const EMPTY_PACKAGE_JSON = JSON.stringify({
FILE: app/lib/plugins/npm.js
constant API_BASE (line 14) | const API_BASE = 'http://registry.npmjs.org/'
method install (line 80) | async install(name, options = {}) {
method update (line 111) | update(name) {
method uninstall (line 125) | async uninstall(name) {
FILE: app/lib/plugins/settings/validate.js
constant VALID_TYPES (line 3) | const VALID_TYPES = new Set([
FILE: app/lib/rpc.js
constant CHANNEL (line 10) | const CHANNEL = 'message'
FILE: app/lib/themes.ts
type Theme (line 3) | type Theme = { value: string, label: string}
FILE: app/main/actions/search.js
constant DEFAULT_SCOPE (line 28) | const DEFAULT_SCOPE = {
function onResultFound (line 72) | function onResultFound(term, result) {
function reset (line 87) | function reset() {
function updateTerm (line 97) | function updateTerm(term) {
function moveCursor (line 127) | function moveCursor(diff) {
function selectElement (line 139) | function selectElement(index) {
function hideElement (line 151) | function hideElement(id) {
function updateElement (line 163) | function updateElement(id, result) {
function changeVisibleResults (line 173) | function changeVisibleResults(count) {
FILE: app/main/actions/statusBar.ts
function reset (line 5) | function reset(): { type: string, payload: null } {
function setValue (line 12) | function setValue(text: string): { type: string, payload: string } {
FILE: app/main/components/Cerebro/index.js
function Autocomplete (line 99) | function Autocomplete({ autocompleteCalculator }) {
function Cerebro (line 117) | function Cerebro({
function mapStateToProps (line 382) | function mapStateToProps(state) {
function mapDispatchToProps (line 393) | function mapDispatchToProps(dispatch) {
FILE: app/main/components/ResultsList/Row/index.tsx
type RowProps (line 7) | interface RowProps {
function Row (line 17) | function Row({
FILE: app/main/components/ResultsList/index.js
function ResultsList (line 9) | function ResultsList({
FILE: app/main/components/SmartIcon/index.tsx
type IconProps (line 7) | interface IconProps {
function FileIcon (line 34) | function FileIcon({ className, path }:IconProps) {
function SmartIcon (line 49) | function SmartIcon({ className, path }: IconProps) {
FILE: app/main/components/StatusBar/index.tsx
type StatusBarProps (line 6) | interface StatusBarProps {
function StatusBar (line 9) | function StatusBar({ value }: StatusBarProps) {
FILE: app/main/constants/actionTypes.ts
constant UPDATE_TERM (line 1) | const UPDATE_TERM = 'UPDATE_TERM'
constant MOVE_CURSOR (line 2) | const MOVE_CURSOR = 'MOVE_CURSOR'
constant SELECT_ELEMENT (line 3) | const SELECT_ELEMENT = 'SELECT_ELEMENT'
constant SHOW_RESULT (line 4) | const SHOW_RESULT = 'SHOW_RESULT'
constant HIDE_RESULT (line 5) | const HIDE_RESULT = 'HIDE_RESULT'
constant UPDATE_RESULT (line 6) | const UPDATE_RESULT = 'UPDATE_RESULT'
constant RESET (line 7) | const RESET = 'RESET'
constant CHANGE_VISIBLE_RESULTS (line 8) | const CHANGE_VISIBLE_RESULTS = 'CHANGE_VISIBLE_RESULTS'
constant ICON_LOADED (line 9) | const ICON_LOADED = 'ICON_LOADED'
constant SET_STATUS_BAR_TEXT (line 10) | const SET_STATUS_BAR_TEXT = 'SET_STATUS_BAR_TEXT'
FILE: app/main/constants/ui.ts
constant INPUT_HEIGHT (line 2) | const INPUT_HEIGHT = 45
constant RESULT_HEIGHT (line 5) | const RESULT_HEIGHT = 45
constant WINDOW_WIDTH (line 8) | const WINDOW_WIDTH = 650
constant MAX_RESULTS (line 11) | const MAX_RESULTS = 25
constant MIN_VISIBLE_RESULTS (line 14) | const MIN_VISIBLE_RESULTS = 10
FILE: app/main/createWindow/AppTray.js
class AppTray (line 9) | class AppTray {
method constructor (line 17) | constructor(options) {
method show (line 25) | show() {
method setIsDev (line 32) | setIsDev(isDev) {
method buildMenu (line 39) | buildMenu() {
method hide (line 105) | hide() {
FILE: app/main/createWindow/buildMenu.js
method click (line 34) | click() { app.quit() }
method click (line 76) | click() {
method click (line 104) | click() { shell.openExternal('http://electron.atom.io') }
method click (line 108) | click() { shell.openExternal('https://github.com/atom/electron/tree/mast...
method click (line 112) | click() { shell.openExternal('https://discuss.atom.io/c/electron') }
method click (line 116) | click() { shell.openExternal('https://github.com/atom/electron/issues') }
FILE: app/main/createWindow/checkForUpdates.js
constant DEFAULT_DOWNLOAD_URL (line 5) | const DEFAULT_DOWNLOAD_URL = 'https://github.com/cerebroapp/cerebro/rele...
constant TITLE (line 7) | const TITLE = 'Cerebro Updates'
constant PLATFORM_EXTENSIONS (line 9) | const PLATFORM_EXTENSIONS = {
FILE: app/main/reducers/search.js
function normalizeSelection (line 40) | function normalizeSelection(index, length) {
function normalizeResult (line 48) | function normalizeResult(result) {
function search (line 57) | function search(stateParam, action) {
FILE: app/main/reducers/statusBar.js
function search (line 11) | function search(stateParam, action) {
FILE: app/main/store/configureStore.js
function configureStore (line 7) | function configureStore(initialState) {
FILE: app/plugins/core/plugins/Preview/ActionButton.js
function ActionButton (line 5) | function ActionButton({ action, onComplete, text }) {
FILE: app/plugins/core/plugins/Preview/FormItem.js
function FormItem (line 12) | function FormItem({
FILE: app/plugins/core/plugins/Preview/Settings.js
function Settings (line 7) | function Settings({ settings, name }) {
FILE: app/plugins/core/plugins/Preview/index.js
function Description (line 13) | function Description({ repoName }) {
function Preview (line 38) | function Preview({ onComplete, plugin }) {
FILE: app/plugins/core/plugins/StatusBar/index.js
function StatusBar (line 5) | function StatusBar({ value }) {
FILE: app/plugins/core/plugins/getAvailablePlugins.js
constant URL (line 5) | const URL = 'https://registry.npmjs.com/-/v1/search?from=0&size=500&text...
FILE: app/plugins/core/plugins/initializeAsync.js
constant OS_APPS_PLUGIN (line 9) | const OS_APPS_PLUGIN = {
constant DEFAULT_PLUGINS (line 14) | const DEFAULT_PLUGINS = [
function checkForUpdates (line 26) | async function checkForUpdates() {
function migratePlugins (line 49) | async function migratePlugins(sendMessage) {
FILE: app/plugins/core/quit/index.js
constant KEYWORDS (line 5) | const KEYWORDS = ['Quit', 'Exit']
FILE: app/plugins/core/settings/Settings/Hotkey.js
constant ASCII (line 5) | const ASCII = {
constant SHIFT_UPS (line 21) | const SHIFT_UPS = {
constant KEYCODES (line 45) | const KEYCODES = {
function Hotkey (line 105) | function Hotkey({ hotkey, onChange }) {
FILE: app/plugins/core/settings/Settings/index.js
function Settings (line 14) | function Settings({ get, set }) {
FILE: app/plugins/core/settings/index.js
constant NAME (line 7) | const NAME = 'Cerebro Settings'
constant KEYWORDS (line 13) | const KEYWORDS = [
FILE: app/plugins/core/version/index.js
constant NAME (line 6) | const NAME = 'Cerebro Version'
constant KEYWORDS (line 12) | const KEYWORDS = [
FILE: server.js
constant PORT (line 10) | const PORT = 3000
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (162K chars).
[
{
"path": ".commitlintrc.json",
"chars": 61,
"preview": "{\n \"extends\": [\n \"@commitlint/config-conventional\"\n ]\n}\n"
},
{
"path": ".editorconfig",
"chars": 328,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".eslintrc.json",
"chars": 1544,
"preview": "{\n \"parser\": \"@babel/eslint-parser\",\n \"extends\": \"airbnb\",\n \"env\": {\n \"browser\": true,\n \"node\": true,\n \"jest"
},
{
"path": ".gitattributes",
"chars": 12,
"preview": "* text=auto\n"
},
{
"path": ".github/issue_template.md",
"chars": 1240,
"preview": "<!--\n Hi there! Thank you for discovering and submitting an issue.\n\n Before you submit this; let's make sure of a few "
},
{
"path": ".github/pull_request_template.md",
"chars": 0,
"preview": ""
},
{
"path": ".github/workflows/build.yml",
"chars": 874,
"preview": "name: Build/release\n\non:\n push:\n tags:\n - '*'\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - us"
},
{
"path": ".github/workflows/pr.yml",
"chars": 375,
"preview": "name: Run Tests\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n test:\n runs-o"
},
{
"path": ".gitignore",
"chars": 780,
"preview": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nl"
},
{
"path": ".husky/commit-msg",
"chars": 88,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no -- commitlint --edit ${1}\n"
},
{
"path": ".vscode/settings.json",
"chars": 328,
"preview": "{\n \"search.exclude\": {\n \".git\": true,\n \".eslintcache\": true,\n \"app/dist\": true,\n \"app/main.prod.js\": true,\n"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "The MIT License (MIT)\n\nCopyright (c) Alexandr Subbotin\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 7263,
"preview": "# Cerebro\n\n> Cerebro is an open-source launcher to improve your productivity and efficiency\n\n<img src=\"./build/icons/128"
},
{
"path": "__mocks__/@electron/remote.js",
"chars": 83,
"preview": "module.exports = {\n app: {\n getPath: () => '',\n getLocale: () => '',\n },\n}\n"
},
{
"path": "__mocks__/electron-store.js",
"chars": 80,
"preview": "class Store {\n get() {\n return {}\n }\n\n set() {}\n}\n\nmodule.exports = Store\n"
},
{
"path": "__mocks__/electron.js",
"chars": 125,
"preview": "module.exports = {\n app: {\n getPath: jest.fn(),\n getLocale: jest.fn(),\n },\n ipcRenderer: {\n on: jest.fn(),\n "
},
{
"path": "__mocks__/fileMock.js",
"chars": 20,
"preview": "module.exports = ''\n"
},
{
"path": "__mocks__/plugins.js",
"chars": 61,
"preview": "module.exports = {\n 'test-plugin': {\n fn: () => {}\n }\n}\n"
},
{
"path": "app/background/background.js",
"chars": 995,
"preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport plugins from 'plugins'\nimport { on, send } from 'lib/r"
},
{
"path": "app/background/createWindow.js",
"chars": 363,
"preview": "import { BrowserWindow } from 'electron'\n\nexport default ({ src }) => {\n const backgroundWindow = new BrowserWindow({\n "
},
{
"path": "app/background/index.html",
"chars": 572,
"preview": "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"Content-Security-Policy\" content=\"scrip"
},
{
"path": "app/initAutoUpdater.js",
"chars": 537,
"preview": "import { autoUpdater } from 'electron-updater'\n\nconst event = 'update-downloaded'\n\nconst TEN_SECONDS = 10 * 1000\nconst O"
},
{
"path": "app/lib/__tests__/loadThemes.spec.js",
"chars": 303,
"preview": "import themes from '../themes'\n\nconst productionThemes = [\n {\n value: '../dist/main/css/themes/light.css',\n label"
},
{
"path": "app/lib/config.js",
"chars": 1733,
"preview": "import { ipcRenderer } from 'electron'\nimport Store from 'electron-store'\nimport themes from './themes'\n\nconst schema = "
},
{
"path": "app/lib/initPlugin.js",
"chars": 854,
"preview": "import { send } from 'lib/rpc'\nimport { settings as pluginSettings } from 'lib/plugins'\n\n/**\n * Initialices plugin sync "
},
{
"path": "app/lib/initializePlugins.js",
"chars": 588,
"preview": "import { on } from 'lib/rpc'\nimport plugins from 'plugins'\nimport initPlugin from './initPlugin'\n\n/**\n * Initialize all "
},
{
"path": "app/lib/plugins/index.js",
"chars": 826,
"preview": "import path from 'path'\nimport fs from 'fs'\nimport npm from './npm'\n\nconst ensureFile = (src, content = '') => {\n if (!"
},
{
"path": "app/lib/plugins/npm.js",
"chars": 4328,
"preview": "import fs from 'fs'\nimport os from 'os'\nimport path from 'path'\nimport tar from 'tar-fs'\nimport zlib from 'zlib'\nimport "
},
{
"path": "app/lib/plugins/settings/__tests__/get.spec.js",
"chars": 433,
"preview": "import getUserSettings from '../get'\n\nconst plugin = {\n settings: {\n test_setting1: {\n type: 'string',\n de"
},
{
"path": "app/lib/plugins/settings/__tests__/validate.spec.js",
"chars": 1837,
"preview": "import validate from '../validate'\n\nconst validSettings = {\n option1: {\n description: 'Just a test description',\n "
},
{
"path": "app/lib/plugins/settings/get.js",
"chars": 1227,
"preview": "import config from 'lib/config'\n\n/**\n * Returns the settings established by the user and previously saved in the config "
},
{
"path": "app/lib/plugins/settings/index.js",
"chars": 116,
"preview": "import getUserSettings from './get'\nimport validate from './validate'\n\nexport default { getUserSettings, validate }\n"
},
{
"path": "app/lib/plugins/settings/validate.js",
"chars": 477,
"preview": "import { every } from 'lodash/fp'\n\nconst VALID_TYPES = new Set([\n 'string',\n 'number',\n 'bool',\n 'option',\n])\n\nconst"
},
{
"path": "app/lib/rpc.js",
"chars": 752,
"preview": "import { ipcRenderer } from 'electron'\nimport EventEmitter from 'events'\n\nconst emitter = new EventEmitter()\n\n/**\n * Cha"
},
{
"path": "app/lib/themes.ts",
"chars": 353,
"preview": "const prefix = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/' : '../'\n\ntype Theme = { value: string, "
},
{
"path": "app/main/actions/__tests__/search.spec.js",
"chars": 1484,
"preview": "/**\n * @jest-environment jsdom\n */\n\nimport {\n MOVE_CURSOR,\n SELECT_ELEMENT,\n UPDATE_RESULT,\n HIDE_RESULT,\n RESET,\n}"
},
{
"path": "app/main/actions/__tests__/statusBar.spec.js",
"chars": 528,
"preview": "/**\n * @jest-environment jsdom\n */\n\nimport {\n SET_STATUS_BAR_TEXT\n} from 'main/constants/actionTypes'\n\nimport * as acti"
},
{
"path": "app/main/actions/search.js",
"chars": 4057,
"preview": "import plugins from 'plugins'\nimport config from 'lib/config'\nimport { shell, clipboard } from 'electron'\n\nimport { sett"
},
{
"path": "app/main/actions/statusBar.ts",
"chars": 334,
"preview": "import {\n SET_STATUS_BAR_TEXT\n} from '../constants/actionTypes'\n\nexport function reset(): { type: string, payload: null"
},
{
"path": "app/main/components/Cerebro/index.js",
"chars": 11040,
"preview": "/* eslint default-case: 0 */\n\nimport React, {\n useEffect, useRef, useState\n} from 'react'\nimport PropTypes from 'prop-t"
},
{
"path": "app/main/components/Cerebro/styles.module.css",
"chars": 1494,
"preview": ".search {\n position: relative;\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n.inputWrapper {\n position"
},
{
"path": "app/main/components/ResultsList/Row/index.tsx",
"chars": 911,
"preview": "import React from 'react'\nimport SmartIcon from '../../SmartIcon'\n\n// @ts-ignore\nimport styles from './styles.module.css"
},
{
"path": "app/main/components/ResultsList/Row/styles.module.css",
"chars": 1091,
"preview": "/**\n * TODO: colors should be moved to variables\n */\n.row {\n position: relative;\n display: flex;\n flex-wrap: nowrap;\n"
},
{
"path": "app/main/components/ResultsList/index.js",
"chars": 2701,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport { List } from 'react-virtualized'\nimport { RESULT_HE"
},
{
"path": "app/main/components/ResultsList/styles.module.css",
"chars": 1986,
"preview": ".wrapper {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n border-top: var(--main-border);\n height: 100%"
},
{
"path": "app/main/components/SmartIcon/getFileIcon/index.ts",
"chars": 463,
"preview": "const { memoize } = require('cerebro-tools')\n\nconst empty = () => Promise.reject()\n\n/* eslint-disable global-require */\n"
},
{
"path": "app/main/components/SmartIcon/getFileIcon/mac.ts",
"chars": 1045,
"preview": "const remote = require('@electron/remote')\n\n/**\n * Get system icon for file\n *\n * @param {String} path File path\n * @pa"
},
{
"path": "app/main/components/SmartIcon/getFileIcon/windows.ts",
"chars": 344,
"preview": "const remote = require('@electron/remote')\n\n/**\n * Get system icon for file\n *\n * @param {String} path File path\n * @re"
},
{
"path": "app/main/components/SmartIcon/index.tsx",
"chars": 1693,
"preview": "import React, { memo } from 'react'\nimport FontAwesome from 'react-fontawesome'\n\n// @ts-ignore\nimport getFileIcon from '"
},
{
"path": "app/main/components/StatusBar/index.tsx",
"chars": 262,
"preview": "import React from 'react'\n\n// @ts-ignore\nimport styles from './styles.module.css'\n\ninterface StatusBarProps {\n value?: "
},
{
"path": "app/main/components/StatusBar/styles.module.css",
"chars": 269,
"preview": ".statusBar {\n position: absolute;\n bottom: 0;\n right: 0;\n padding: 5px;\n border-radius: 5px 0 0 0;\n border: var(--"
},
{
"path": "app/main/constants/actionTypes.ts",
"chars": 446,
"preview": "export const UPDATE_TERM = 'UPDATE_TERM'\nexport const MOVE_CURSOR = 'MOVE_CURSOR'\nexport const SELECT_ELEMENT = 'SELECT_"
},
{
"path": "app/main/constants/ui.ts",
"chars": 352,
"preview": "// Height of main input\nexport const INPUT_HEIGHT = 45\n\n// Heigth of default result line\nexport const RESULT_HEIGHT = 45"
},
{
"path": "app/main/createWindow/AppTray.js",
"chars": 2600,
"preview": "import { Menu, Tray, app } from 'electron'\nimport showWindowWithTerm from './showWindowWithTerm'\nimport toggleWindow fro"
},
{
"path": "app/main/createWindow/autoStart.js",
"chars": 693,
"preview": "import { app } from 'electron'\nimport AutoLaunch from 'auto-launch'\n\nconst isLinux = !['win32', 'darwin'].includes(proce"
},
{
"path": "app/main/createWindow/buildMenu.js",
"chars": 2571,
"preview": "import { Menu, shell, app } from 'electron'\n\nexport default (mainWindow) => {\n const template = [{\n label: 'Electron"
},
{
"path": "app/main/createWindow/checkForUpdates.js",
"chars": 1548,
"preview": "import { dialog, app, shell } from 'electron'\nimport { autoUpdater } from 'electron-updater'\n\nconst currentVersion = app"
},
{
"path": "app/main/createWindow/handleUrl.js",
"chars": 480,
"preview": "import { parse } from 'url'\n\nimport showWindowWithTerm from './showWindowWithTerm'\n\nexport default (mainWindow, url) => "
},
{
"path": "app/main/createWindow/showWindowWithTerm.ts",
"chars": 275,
"preview": "/**\n * Show main window with updated search term\n *\n * @return {BrowserWindow} appWindow\n */\nexport default (appWindow: "
},
{
"path": "app/main/createWindow/toggleWindow.ts",
"chars": 372,
"preview": "/**\n * Show or hide main window\n * @return {BrowserWindow} appWindow\n */\nexport default (appWindow: any) => {\n if (appW"
},
{
"path": "app/main/createWindow.js",
"chars": 4837,
"preview": "import {\n BrowserWindow, globalShortcut, app, shell\n} from 'electron'\nimport debounce from 'lodash/debounce'\nimport Eve"
},
{
"path": "app/main/css/global.css",
"chars": 375,
"preview": "@import \"system-font.css\";\n@import url(\"~normalize.css/normalize.css\");\n@import url(\"~react-virtualized/styles.css\");\n\nh"
},
{
"path": "app/main/css/system-font.css",
"chars": 2566,
"preview": "/*! system-font.css v1.1.0 | CC0-1.0 License | github.com/jonathantneal/system-font-face */\n\n@font-face {\n font-family:"
},
{
"path": "app/main/css/themes/dark.css",
"chars": 1202,
"preview": ":root {\n /* Main fonts and colors */\n --main-background-color: rgba(62, 65, 67, 1);\n --main-font: system, sans-serif;"
},
{
"path": "app/main/css/themes/light.css",
"chars": 1185,
"preview": ":root {\n /* Main fonts and colors */\n --main-background-color: rgba(255, 255, 255, 1);\n --main-font: system, sans-ser"
},
{
"path": "app/main/index.html",
"chars": 972,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"Content-Security-Policy\" content=\"scrip"
},
{
"path": "app/main/main.js",
"chars": 1258,
"preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport { Provider } from 'react-redux'\n\nimport initializePlug"
},
{
"path": "app/main/reducers/index.js",
"chars": 198,
"preview": "import { combineReducers } from 'redux'\nimport search from './search'\nimport statusBar from './statusBar'\n\nconst rootRed"
},
{
"path": "app/main/reducers/search.js",
"chars": 3685,
"preview": "/* eslint no-shadow: [2, { \"allow\": [\"comments\"] }] */\nimport uniq from 'lodash/uniq'\nimport orderBy from 'lodash/orderB"
},
{
"path": "app/main/reducers/statusBar.js",
"chars": 477,
"preview": "/* eslint no-shadow: [2, { \"allow\": [\"comments\"] }] */\n\nimport {\n SET_STATUS_BAR_TEXT\n} from 'main/constants/actionType"
},
{
"path": "app/main/store/configureStore.js",
"chars": 280,
"preview": "import { createStore, applyMiddleware } from 'redux'\nimport thunk from 'redux-thunk'\nimport rootReducer from '../reducer"
},
{
"path": "app/main/store/index.ts",
"chars": 79,
"preview": "import configureStore from './configureStore'\n\nexport default configureStore()\n"
},
{
"path": "app/main.development.js",
"chars": 3728,
"preview": "import { app, ipcMain } from 'electron'\nimport path from 'path'\n\nimport createMainWindow from './main/createWindow'\nimpo"
},
{
"path": "app/package.json",
"chars": 1374,
"preview": "{\n \"name\": \"cerebro\",\n \"productName\": \"Cerebro\",\n \"description\": \"Cerebro is an open-source launcher to improve your "
},
{
"path": "app/plugins/core/autocomplete/index.js",
"chars": 1221,
"preview": "import { search } from 'cerebro-tools'\nimport {\n flow, filter, map, partialRight, values\n} from 'lodash/fp'\nimport exte"
},
{
"path": "app/plugins/core/index.ts",
"chars": 273,
"preview": "import autocomplete from './autocomplete'\nimport quit from './quit'\nimport plugins from './plugins'\nimport settings from"
},
{
"path": "app/plugins/core/plugins/Preview/ActionButton.js",
"chars": 516,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport { KeyboardNavItem } from '@cerebroapp/cerebro-ui'\n\nf"
},
{
"path": "app/plugins/core/plugins/Preview/FormItem.js",
"chars": 950,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport { FormComponents } from '@cerebroapp/cerebro-ui'\n\nco"
},
{
"path": "app/plugins/core/plugins/Preview/Settings.js",
"chars": 1261,
"preview": "import React, { useState, useEffect } from 'react'\nimport PropTypes from 'prop-types'\nimport config from 'lib/config'\nim"
},
{
"path": "app/plugins/core/plugins/Preview/index.js",
"chars": 3513,
"preview": "import React, { useState, useEffect } from 'react'\nimport PropTypes from 'prop-types'\nimport { KeyboardNav, KeyboardNavI"
},
{
"path": "app/plugins/core/plugins/Preview/styles.module.css",
"chars": 1676,
"preview": ".preview {\n align-self: flex-start;\n width: 100%;\n}\n\n.header {\n border-bottom: var(--main-border);\n margin-bottom: 1"
},
{
"path": "app/plugins/core/plugins/StatusBar/index.js",
"chars": 284,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport styles from './styles.module.css'\n\nfunction StatusBa"
},
{
"path": "app/plugins/core/plugins/StatusBar/styles.module.css",
"chars": 360,
"preview": ".statusBar {\n position: absolute;\n right: 0;\n bottom: 0;\n z-index: 11;\n font-size: 11px;\n color: var(--secondary-f"
},
{
"path": "app/plugins/core/plugins/blacklist.js",
"chars": 406,
"preview": "/**\n * This file contains plugins that have been blacklisted.\n * The main purpose of this is to hide plugins that have b"
},
{
"path": "app/plugins/core/plugins/format.js",
"chars": 906,
"preview": "import {\n flow, words, capitalize, trim, map, join\n} from 'lodash/fp'\n\n/**\n * Remove unnecessary information from plugi"
},
{
"path": "app/plugins/core/plugins/getAvailablePlugins.js",
"chars": 841,
"preview": "/**\n * API endpoint to search all cerebro plugins\n * @type {String}\n */\nconst URL = 'https://registry.npmjs.com/-/v1/sea"
},
{
"path": "app/plugins/core/plugins/getDebuggingPlugins.js",
"chars": 1502,
"preview": "import path from 'path'\nimport { modulesDirectory } from 'lib/plugins'\nimport { lstatSync, readdirSync } from 'fs'\n\ncons"
},
{
"path": "app/plugins/core/plugins/getInstalledPlugins.js",
"chars": 776,
"preview": "import { packageJsonPath } from 'lib/plugins'\nimport { readFile } from 'fs/promises'\nimport externalPlugins from 'plugin"
},
{
"path": "app/plugins/core/plugins/getReadme.js",
"chars": 322,
"preview": "/**\n * Get plugin Readme.md content\n *\n * @param {String} repository Repository field from npm package\n * @return {Prom"
},
{
"path": "app/plugins/core/plugins/index.js",
"chars": 2937,
"preview": "import React from 'react'\nimport { search } from 'cerebro-tools'\nimport { shell } from 'electron'\nimport { partition } f"
},
{
"path": "app/plugins/core/plugins/initializeAsync.js",
"chars": 2000,
"preview": "import { client } from 'lib/plugins'\nimport config from 'lib/config'\nimport {\n flow, filter, map, property\n} from 'loda"
},
{
"path": "app/plugins/core/plugins/loadPlugins.js",
"chars": 1887,
"preview": "import { memoize } from 'cerebro-tools'\nimport validVersion from 'semver/functions/valid'\nimport compareVersions from 's"
},
{
"path": "app/plugins/core/quit/index.js",
"chars": 553,
"preview": "import { ipcRenderer } from 'electron'\nimport { search } from 'cerebro-tools'\nimport icon from '../icon.png'\n\nconst KEYW"
},
{
"path": "app/plugins/core/reload/index.js",
"chars": 568,
"preview": "import { ipcRenderer } from 'electron'\nimport icon from '../icon.png'\n\nconst keyword = 'reload'\nconst title = 'Reload'\nc"
},
{
"path": "app/plugins/core/settings/Settings/Hotkey.js",
"chars": 2901,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport styles from './styles.module.css'\n\nconst ASCII = {\n "
},
{
"path": "app/plugins/core/settings/Settings/countries.js",
"chars": 9971,
"preview": "export default [\n { value: 'AF', label: 'Afghanistan' },\n { value: 'AX', label: 'Åland Islands' },\n { value: 'AL', la"
},
{
"path": "app/plugins/core/settings/Settings/index.js",
"chars": 2976,
"preview": "import React, { useState } from 'react'\nimport PropTypes from 'prop-types'\nimport { FormComponents } from '@cerebroapp/c"
},
{
"path": "app/plugins/core/settings/Settings/styles.module.css",
"chars": 629,
"preview": ".settings {\n display: flex;\n align-self: flex-start;\n flex-direction: column;\n align-items: center;\n}\n\n.label {\n ma"
},
{
"path": "app/plugins/core/settings/index.js",
"chars": 1074,
"preview": "import React from 'react'\nimport { search } from 'cerebro-tools'\nimport Settings from './Settings'\nimport icon from '../"
},
{
"path": "app/plugins/core/version/index.js",
"chars": 942,
"preview": "import React from 'react'\nimport { search } from 'cerebro-tools'\nimport icon from '../icon.png'\n\n// Settings plugin name"
},
{
"path": "app/plugins/externalPlugins.js",
"chars": 3416,
"preview": "import debounce from 'lodash/debounce'\nimport chokidar from 'chokidar'\nimport path from 'path'\nimport initPlugin from 'l"
},
{
"path": "app/plugins/index.ts",
"chars": 225,
"preview": "import core from './core'\nimport externalPlugins from './externalPlugins'\n\nconst pluginsService = {\n corePlugins: core,"
},
{
"path": "babel.config.js",
"chars": 342,
"preview": "module.exports = {\n presets: [\n '@babel/preset-typescript',\n [\n '@babel/preset-env', {\n /** Targets m"
},
{
"path": "build/installer.nsh",
"chars": 460,
"preview": "!macro customInstall\n DetailPrint \"Register cerebro URI Handler\"\n DeleteRegKey HKCR \"cerebro\"\n WriteRegStr HKCR \"cere"
},
{
"path": "electron-builder.json",
"chars": 2048,
"preview": "{\n \"productName\": \"Cerebro\",\n \"appId\": \"com.cerebroapp.Cerebro\",\n \"protocols\": {\n \"name\": \"Cerebro URLs\",\n \"rol"
},
{
"path": "jest.config.js",
"chars": 304,
"preview": "module.exports = {\n collectCoverage: true,\n moduleDirectories: ['node_modules', 'app'],\n moduleNameMapper: {\n '\\\\."
},
{
"path": "package.json",
"chars": 3558,
"preview": "{\n \"name\": \"cerebro\",\n \"productName\": \"cerebro\",\n \"version\": \"0.11.0\",\n \"description\": \"Cerebro is an open-source la"
},
{
"path": "postcss.config.js",
"chars": 85,
"preview": "module.exports = {\n plugins: {\n 'postcss-nested': {},\n autoprefixer: {}\n }\n}\n"
},
{
"path": "server.js",
"chars": 725,
"preview": "const express = require('express')\nconst webpack = require('webpack')\nconst webpackDevMiddleware = require('webpack-dev-"
},
{
"path": "tsconfig.json",
"chars": 227,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \"app\",\n \"jsx\": \"react\",\n \"allowJs\": true,\n \"noImplici"
},
{
"path": "webpack.config.base.js",
"chars": 881,
"preview": "const path = require('path')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\nconst LodashModuleReplacementPlugi"
},
{
"path": "webpack.config.development.js",
"chars": 1379,
"preview": "const webpack = require('webpack')\nconst baseConfig = require('./webpack.config.base')\n\nconst config = {\n ...baseConfig"
},
{
"path": "webpack.config.electron.js",
"chars": 371,
"preview": "const baseConfig = require('./webpack.config.base')\n\nmodule.exports = {\n ...baseConfig,\n module: {\n rules: [{\n "
},
{
"path": "webpack.config.production.js",
"chars": 1313,
"preview": "const path = require('path')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst Visualizer = require("
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the cerebroapp/cerebro GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (145.0 KB), approximately 42.4k tokens, and a symbol index with 93 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.