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