[
  {
    "path": ".gitignore",
    "content": "# Rendering data\nrender/frames/*\nrender/data.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Dependency directories\nnode_modules\n\n# Optional npm cache directory\n.npm\n\n# Build\nrender/dist\n\n# Private assets\ndemo\nlogo\n\n# Others\n.DS_Store\n"
  },
  {
    "path": ".jscsrc",
    "content": "{\n  \"requireCurlyBraces\": [\n    \"if\",\n    \"else\",\n    \"for\",\n    \"while\",\n    \"do\",\n    \"try\",\n    \"catch\"\n  ],\n  \"requireSpaceAfterKeywords\": [\n    \"if\",\n    \"else\",\n    \"for\",\n    \"while\",\n    \"do\",\n    \"switch\",\n    \"return\",\n    \"try\",\n    \"catch\"\n  ],\n  \"requireSemicolons\": true,\n  \"requireSpacesInForStatement\": true,\n  \"requireSpaceBeforeBlockStatements\": true,\n  \"requireParenthesesAroundIIFE\": true,\n  \"requireSpacesInConditionalExpression\": true,\n  \"requireSpacesInAnonymousFunctionExpression\": {\n    \"beforeOpeningCurlyBrace\": true\n  },\n  \"requireSpacesInNamedFunctionExpression\": {\n    \"beforeOpeningCurlyBrace\": true\n  },\n  \"requireBlocksOnNewline\": true,\n  \"disallowEmptyBlocks\": false,\n  \"disallowSpacesInsideObjectBrackets\": true,\n  \"disallowSpacesInsideArrayBrackets\": true,\n  \"disallowSpacesInsideParentheses\": true,\n  \"requireSpaceAfterComma\": true,\n  \"disallowSpaceAfterPrefixUnaryOperators\": [\n    \"++\",\n    \"--\",\n    \"+\",\n    \"-\",\n    \"~\",\n    \"!\"\n  ],\n  \"disallowSpaceBeforePostfixUnaryOperators\": [\n    \"++\",\n    \"--\"\n  ],\n  \"requireSpaceBeforeBinaryOperators\": [\n    \"=\",\n    \"+=\",\n    \"-=\",\n    \"*=\",\n    \"/=\",\n    \"%=\",\n    \"<<=\",\n    \">>=\",\n    \">>>=\",\n    \"&=\",\n    \"|=\",\n    \"^=\",\n    \"+\",\n    \"-\",\n    \"*\",\n    \"/\",\n    \"%\",\n    \"<<\",\n    \">>\",\n    \">>>\",\n    \"&\",\n    \"|\",\n    \"^\",\n    \"&&\",\n    \"||\",\n    \"===\",\n    \"==\",\n    \">=\",\n    \"<=\",\n    \"<\",\n    \">\",\n    \"!=\",\n    \"!==\"\n  ],\n  \"requireSpaceAfterBinaryOperators\": true,\n  \"requireCamelCaseOrUpperCaseIdentifiers\": {\n    \"ignoreProperties\": true\n  },\n  \"disallowKeywords\": [\n    \"with\"\n  ],\n  \"disallowMultipleLineStrings\": true,\n  \"validateLineBreaks\": \"LF\",\n  \"validateIndentation\": 2,\n  \"disallowTrailingComma\": true,\n  \"requireLineFeedAtFileEnd\": true,\n  \"validateQuoteMarks\": {\n    \"mark\": \"'\",\n    \"escape\": true\n  },\n  \"requireCapitalizedComments\": true,\n  \"requireSpaceAfterLineComment\": {\n    \"allExcept\": [\n      \"//////////////////////////////////////////////////\"\n    ]\n  },\n  \"jsDoc\": {\n    \"checkAnnotations\": true,\n    \"checkRedundantAccess\": true,\n    \"checkTypes\": \"capitalizedNativeCase\",\n    \"requireNewlineAfterDescription\": true,\n    \"checkParamExistence\": true,\n    \"checkParamNames\": true,\n    \"requireParamTypes\": true,\n    \"checkRedundantParams\": true,\n    \"requireReturnTypes\": true\n  }\n}\n"
  },
  {
    "path": ".npmignore",
    "content": "# Rendering data\nrender/frames/*\nrender/data.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Dependency directories\nnode_modules\n\n# Optional npm cache directory\n.npm\n\n# Whitelist build\n!dist\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"printWidth\": 120\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Mohammad Fares\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": "<p align=\"center\">\n  <a href=\"https://www.terminalizer.com\">\n    <img src=\"/img/logo.png?raw=true\" width=\"200\"/>\n  </a>\n</p>\n\n# Terminalizer\n\n[![npm](https://img.shields.io/npm/v/terminalizer.svg)](https://www.npmjs.com/package/terminalizer)\n[![npm](https://img.shields.io/npm/l/terminalizer.svg)](https://github.com/faressoft/terminalizer/blob/master/LICENSE)\n[![Gitter](https://badges.gitter.im/join_chat.svg)](https://gitter.im/terminalizer/Lobby)\n[![Unicorn](https://img.shields.io/badge/nyancat-approved-ff69b4.svg)](https://www.youtube.com/watch?v=QH2-TGUlwu4)\n[![Tweet](https://img.shields.io/badge/twitter-share-76abec.svg)](https://goo.gl/QJzJu1)\n\n> Record your terminal and generate animated gif images or share a web player link [www.terminalizer.com](https://www.terminalizer.com)\n\n<p align=\"center\"><img src=\"/img/demo.gif?raw=true\"/></p>\n\nBuilt to be jusT cOol 👌🦄 !\n\n> If you think so, support me with a `star` and a `follow` 😘\n\n---\n\n<p align=\"center\"><img src=\"/img/trending.png?raw=true\"/></p>\n\n---\n\n# Table of Contents\n\n- [Terminalizer](#terminalizer)\n- [Table of Contents](#table-of-contents)\n- [Features](#features)\n- [What's Next](#whats-next)\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n  - [Compression](#compression)\n- [Usage](#usage)\n  - [Init](#init)\n  - [Config](#config)\n  - [Record](#record)\n  - [Play](#play)\n  - [Render](#render)\n  - [Share](#share)\n  - [Generate](#generate)\n- [Configurations](#configurations)\n  - [Recording](#recording)\n  - [Delays](#delays)\n  - [GIF](#gif)\n  - [Terminal](#terminal)\n  - [Theme](#theme)\n  - [Watermark](#watermark)\n  - [Frame Box](#frame-box)\n    - [Null Frame](#null-frame)\n    - [Window Frame](#window-frame)\n    - [Floating Frame](#floating-frame)\n    - [Solid Frame](#solid-frame)\n    - [Solid Frame Without Title](#solid-frame-without-title)\n    - [Styling Hint](#styling-hint)\n- [FAQ](#faq)\n  - [How to support ZSH](#how-to-support-zsh)\n- [Issues](#issues)\n- [License](#license)\n\n# Features\n\n- Highly customizable.\n- Cross platform (Linux, Windows, MacOS).\n- Custom `window frames`.\n- Custom `font`.\n- Custom `colors`.\n- Custom `styles` with `CSS`.\n- Watermark.\n- Edit frames and adjust delays before rendering.\n- Skipping frames by a step value to reduce the number of rendered frames.\n- Render images with texts on them instead of capturing your screen for better quality.\n- The ability to configure:\n  - The command to capture (bash, powershell.exe, yourOwnCommand, etc)\n  - The current working directory.\n  - Explicit values for the number of cols and rows.\n  - GIF quality and repeating.\n  - Frames delays.\n  - The max idle time between frames.\n  - Cursor style.\n  - Font.\n  - Font size.\n  - Line height.\n  - Letter spacing.\n  - Theme.\n\n# What's Next\n\n- The `Generate` command to generate a web player for a recording file.\n- Support `apt-get`, `yum`, `brew` installation.\n\n# Installation\n\nYou need to install [Node.js](https://nodejs.org/en/download/) first, then install the tool globally using this command:\n\n```bash\nyarn global add terminalizer\n```\n\n<p align=\"center\"><img src=\"/img/install.gif?raw=true\"/></p>\n\n> Still facing an issue? Check the [Issues](#issues) section or open a new issue.\n\nThe installation should be very smooth with Node.js v4-v16. For newer versions, if the installation is failed, you may need to install the development tools to build the `C++` add-ons. Check [node-gyp](https://github.com/nodejs/node-gyp#installation).\n\n# Getting Started\n\nStart recording your terminal using the `record` command.\n\n```bash\nterminalizer record demo\n```\n\nA file called `demo.yml` will be created in the current directory. You can open it using any editor to edit the configurations and the recorded frames. You can replay your recording using the `play` command.\n\n```bash\nterminalizer play demo\n```\n\nNow let's render our recording as an animated gif.\n\n```bash\nterminalizer render demo\n```\n\n## Compression\n\nGIF compression is not implemented yet. For now we recommend [https://gifcompressor.com](https://gifcompressor.com).\n\n# Usage\n\n> You can use the `--help` option to get more details about the commands and their options\n\n```bash\nterminalizer <command> [options]\n```\n\n## Init\n\n> Create a global config directory\n\n```bash\nterminalizer init\n```\n\n## Config\n\n> Generate a config file in the current directory\n\n```bash\nterminalizer config\n```\n\n## Record\n\n> Record your terminal and create a recording file\n\n```bash\nterminalizer record <recordingFile>\n```\n\nOptions\n\n```\n-c, --config        Overwrite the default configurations                                  [string]\n-d, --command       The command to be executed                            [string] [default: null]\n-k, --skip-sharing  Skip sharing and showing the sharing prompt message [boolean] [default: false]\n```\n\nExamples\n\n```\nterminalizer record foo                      Start recording and create a recording file called foo.yml\nterminalizer record foo --config config.yml  Start recording with your own configurations\n```\n\n## Play\n\n> Play a recording file on your terminal\n\n```bash\nterminalizer play <recordingFile>\n```\n\nOptions\n\n```\n-r, --real-timing   Use the actual delays between frames as recorded        [boolean] [default: false]\n-s, --speed-factor  Speed factor, multiply the frames delays by this factor [number] [default: 1]\n```\n\n## Render\n\n> Render a recording file as an animated gif image\n\n```bash\nterminalizer render <recordingFile>\n```\n\nOptions\n\n```\n-o, --output   A name for the output file                                      [string]\n-q, --quality  The quality of the rendered image (1 - 100)                     [number]\n-s, --step     To reduce the number of rendered frames (step > 1) [number] [default: 1]\n```\n\n## Share\n\n> Upload a recording file and get a link for an online player\n\n```bash\nterminalizer share <recordingFile>\n```\n\n## Generate\n\n> Generate a web player for a recording file\n\n```bash\nterminalizer generate <recordingFile>\n```\n\n# Configurations\n\nThe default `config.yml` file is stored under the root directory of the project. Execute the below command to copy it to your current directory.\n\n> Use any editor to edit the copied `config.yml`, then use the `-c` option to override the default one.\n\n```bash\nterminalizer config\n```\n\n> RECOMMENDED, use the `init` command to create a global config file to be used instead of the default one.\n\n```bash\nterminalizer init\n```\n\nFor Linux and MacOS, the created directory is located under the home directory `~/config/terminalizer`. For Windows, it is located under the `AppData`.\n\n## Recording\n\n- `command`: Specify a command to be executed like `/bin/bash -l`, `ls`, or any other command. The default is `bash` for `Linux` or `powershell.exe` for `Windows`.\n- `cwd`: Specify the current working directory path. The default is the current working directory path.\n- `env`: Export additional ENV variables, to be read by your scripts when starting the recording.\n- `cols`: Explicitly set the number of columns or use `auto` to take the current number of columns of your shell.\n- `rows`: Explicitly set the number of rows or use `auto` to take the current number of rows of your shell.\n\n## Delays\n\n- `frameDelay`: The delay between frames in ms. If the value is `auto` use the actual recording delays.\n- `maxIdleTime`: Maximum delay between frames in ms. Ignored if the `frameDelay` isn't set to `auto`. Set to `auto` to prevent limiting the max idle time.\n\n## GIF\n\n- `quality`: The quality of the generated GIF image (1 - 100).\n- `repeat`: Amount of times to repeat GIF:\n  - If value is `-1`, play once.\n  - If value is `0`, loop indefinitely.\n  - If value is a positive number, loop `n` times.\n\n## Terminal\n\n- `cursorStyle`: Cursor style can be one of `block`, `underline`, or `bar`.\n- `fontFamily`: You can use any font that is installed on your machine like `Monaco` or `Lucida Console` (CSS-like list).\n- `fontSize`: The size of the font in pixels.\n- `lineHeight`: The height of lines in pixels.\n- `letterSpacing`: The spacing between letters in pixels.\n\n## Theme\n\nYou can set the colors of your terminal using one of the CSS formats:\n\n- Hex: `#FFFFFF`.\n- RGB: `rgb(255, 255, 255)`.\n- HSL: `hsl(0, 0%, 100%)`.\n- Name: `white`, `red`, `blue`.\n\n> You can use the value `transparent` too.\n\nThe default colors that are assigned to the terminal colors are:\n\n- background: ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `transparent`\n- foreground: ![#afafaf](https://placehold.it/15/afafaf/000000?text=+) `#afafaf`\n- cursor: ![#c7c7c7](https://placehold.it/15/c7c7c7/000000?text=+) `#c7c7c7`\n- black: ![#232628](https://placehold.it/15/232628/000000?text=+) `#232628`\n- red: ![#fc4384](https://placehold.it/15/fc4384/000000?text=+) `#fc4384`\n- green: ![#b3e33b](https://placehold.it/15/b3e33b/000000?text=+) `#b3e33b`\n- yellow: ![#ffa727](https://placehold.it/15/ffa727/000000?text=+) `#ffa727`\n- blue: ![#75dff2](https://placehold.it/15/75dff2/000000?text=+) `#75dff2`\n- magenta: ![#ae89fe](https://placehold.it/15/ae89fe/000000?text=+) `#ae89fe`\n- cyan: ![#708387](https://placehold.it/15/708387/000000?text=+) `#708387`\n- white: ![#d5d5d0](https://placehold.it/15/d5d5d0/000000?text=+) `#d5d5d0`\n- brightBlack: ![#626566](https://placehold.it/15/626566/000000?text=+) `#626566`\n- brightRed: ![#ff7fac](https://placehold.it/15/ff7fac/000000?text=+) `#ff7fac`\n- brightGreen: ![#c8ed71](https://placehold.it/15/c8ed71/000000?text=+) `#c8ed71`\n- brightYellow: ![#ebdf86](https://placehold.it/15/ebdf86/000000?text=+) `#ebdf86`\n- brightBlue: ![#75dff2](https://placehold.it/15/75dff2/000000?text=+) `#75dff2`\n- brightMagenta: ![#ae89fe](https://placehold.it/15/ae89fe/000000?text=+) `#ae89fe`\n- brightCyan: ![#b1c6ca](https://placehold.it/15/b1c6ca/000000?text=+) `#b1c6ca`\n- brightWhite: ![#f9f9f4](https://placehold.it/15/f9f9f4/000000?text=+) `#f9f9f4`\n\n## Watermark\n\nYou can add a watermark logo to your generated GIF images.\n\n<p align=\"center\"><img src=\"/img/watermark.gif?raw=true\"/></p>\n\n```\nwatermark:\n  imagePath: AbsolutePathOrURL\n  style:\n    position: absolute\n    right: 15px\n    bottom: 15px\n    width: 100px\n    opacity: 0.9\n```\n\n- `watermark.imagePath`: An absolute path for the image on your machine or a URL.\n- `watermark.style`: Apply CSS styles (camelCase) to the watermark image, like resizing it.\n\n## Frame Box\n\nTerminalizer comes with predefined frames that you can use to make your GIF images look cool.\n\n- `frameBox.type`: Can be `null`, `window`, `floating`, or `solid`.\n- `frameBox.title`: To display a title for the frame or `null`.\n- `frameBox.style`: To apply custom CSS styles or to override the current ones.\n\n### Null Frame\n\nNo frame, just your recording.\n\n<p align=\"center\"><img src=\"/img/frames/null.gif?raw=true\"/></p>\n\n> Don't forget to add a `backgroundColor` under `style`.\n\n```\nframeBox:\n  type: null\n  title: null\n  style:\n    backgroundColor: black\n```\n\n### Window Frame\n\n<p align=\"center\"><img src=\"/img/frames/window.gif?raw=true\"/></p>\n\n```\nframeBox:\n  type: window\n  title: Terminalizer\n  style: []\n```\n\n### Floating Frame\n\n<p align=\"center\"><img src=\"/img/frames/floating.gif?raw=true\"/></p>\n\n```\nframeBox:\n  type: floating\n  title: Terminalizer\n  style: []\n```\n\n### Solid Frame\n\n<p align=\"center\"><img src=\"/img/frames/solid.gif?raw=true\"/></p>\n\n```\nframeBox:\n  type: solid\n  title: Terminalizer\n  style: []\n```\n\n### Solid Frame Without Title\n\n<p align=\"center\"><img src=\"/img/frames/solid_without_title.gif?raw=true\"/></p>\n\n```\nframeBox:\n  type: solid\n  title: null\n  style: []\n```\n\n### Styling Hint\n\nYou can disable the default shadows and margins.\n\n<p align=\"center\"><img src=\"/img/frames/solid_without_title_without_shadows.gif?raw=true\"/></p>\n\n```\nframeBox:\n  type: solid\n  title: null\n  style:\n    boxShadow: none\n    margin: 0px\n```\n\n# FAQ\n\n### How to support ZSH\n\nThe default command that gets recorded for Linux is `bash -l`. You need to change the default command to `zsh`.\n\n- Generate a config file in the current directory\n\n```bash\nterminalizer config\n```\n\n- Open the generated config file in your preferred editor.\n- Change the `command` to `zsh`:\n\n```\ncommand: zsh\n```\n\n- You may need to change the font, check the font that is used in your terminal:\n\n```\nfontFamily: \"Meslo for Powerline, Meslo LG M for Powerline\"\n```\n\n- Use the `-c` option to override the config file:\n\n```bash\nterminalizer record demo -c config.yml\n```\n\n# Issues\n\n> error while loading shared libraries: libXss.so.1: cannot open shared object file: No such file or directory\n\nSolution:\n\n```bash\nsudo yum install libXScrnSaver\n```\n\n> error while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory\n\nSolution:\n\n```bash\nsudo apt-get install libgconf-2-4\n```\n\n> Error: EACCES: permission denied, access '/usr/local/lib'\n\nSolution:\n\n```bash\nsudo mkdir -p /usr/local/lib/node_modules && sudo chown -R $(whoami):$(whoami) /usr/local/lib/node_modules\n\n# then use the install command in the \"Installation\" section above\nyarn global add terminalizer\n```\n\n# License\n\nThis project is under the MIT license.\n"
  },
  {
    "path": "app.js",
    "content": "/**\n * Terminalizer\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nvar yargs = require('yargs'),\n    requireDir = require('require-dir');\nvar package = require('./package.json'),\n    commands = requireDir('./commands'),\n    DI = require('./di.js');\n\n// Define the DI as a global object\nglobal.di = new DI();\n\n// Define the the root path of the app as a global constant\nglobal.ROOT_PATH = __dirname;\n\n// The base url of the Terminalizer website\n// `www` is necessary due to https://github.com/faressoft/terminalizer/issues/207\nglobal.BASEURL = 'https://www.terminalizer.com';\n\n// Dependency Injection\ndi.require('chalk');\ndi.require('async');\ndi.require('axios');\ndi.require('death');\ndi.require('path');\ndi.require('os');\ndi.require('electron');\ndi.require('deepmerge');\ndi.require('uuid');\ndi.require('tmp');\ndi.require('lodash', '_');\ndi.require('fs-extra', 'fs');\ndi.require('js-yaml', 'yaml');\ndi.require('performance-now', 'now');\ndi.require('async-promises', 'asyncPromises');\ndi.require('string-argv', 'stringArgv');\ndi.require('progress', 'ProgressBar');\ndi.require('gif-encoder', 'GIFEncoder');\ndi.require('inquirer');\n\ndi.set('pty', require('@homebridge/node-pty-prebuilt-multiarch'));\ndi.set('PNG', require('pngjs').PNG);\ndi.set('spawn', require('child_process').spawn);\ndi.set('utility', require('./utility.js'));\ndi.set('commands', commands);\ndi.set('errorHandler', errorHandler);\n\n// Initialize yargs\nyargs.usage('Usage: $0 <command> [options]')\n     // Add link\n     .epilogue('For more information, check https://www.terminalizer.com')\n     // Set the version number\n     .version(package.version)\n     // Add aliases for version and help options\n     .alias({v: 'version', h: 'help'})\n     // Require to pass a command\n     .demandCommand(1, 'The command is missing')\n     // Strict mode\n     .strict()\n     // Set width to 90 cols\n     .wrap(100)\n     // Handle failures\n     .fail(errorHandler);\n\n// Load commands\nyargs.command(commands.init)\n     .command(commands.config)\n     .command(commands.record)\n     .command(commands.play)\n     .command(commands.render)\n     .command(commands.share)\n     .command(commands.generate)\n\ndebugger;\n\ntry {\n\n  // Parse the command line arguments\n  yargs.parse();\n\n} catch (error) {\n\n  // Print the error\n  errorHandler(error);\n\n}\n\n/**\n * Print an error\n * \n * @param {String|Error} error\n */\nfunction errorHandler(error) {\n\n  error = error.toString();\n\n  console.error('Error: \\n  ' + error + '\\n');\n  console.error('Hint:\\n  Use the ' + di.chalk.green('--help') + ' option to get help about the usage');\n  process.exit(1);\n\n}\n"
  },
  {
    "path": "bin/app.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Terminalizer\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nrequire('../app.js');\n"
  },
  {
    "path": "commands/config.js",
    "content": "/**\n * Config\n * Generate a config file in the current directory\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Executed after the command completes its task\n */\nfunction done() {\n\n  console.log(di.chalk.green('Successfully Saved'));\n  console.log('The config file is saved into the file:');\n  console.log(di.chalk.magenta('config.yml'));\n\n}\n\n/**\n * The command's main function\n * \n * @param {Object} argv\n */\nfunction command(argv) {\n\n  // Copy the default config file\n  di.fs.copySync(di.path.join(ROOT_PATH, 'config.yml'), 'config.yml');\n\n  done();\n\n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = 'config';\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = 'Generate a config file in the current directory';\n\n/**\n * Command's handler function\n * @type {Function}\n */\nmodule.exports.handler = command;\n"
  },
  {
    "path": "commands/generate.js",
    "content": "/**\n * Generate\n * Generate a web player for a recording file\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Executed after the command completes its task\n */\nfunction done() {\n\n  // Terminate the app\n  process.exit();\n\n}\n\n/**\n * The command's main function\n * \n * @param {Object} argv\n */\nfunction command(argv) {\n\n  console.log('This command is not implemented yet. It will be available in the next versions');\n\n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = 'generate <recordingFile>';\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = 'Generate a web player for a recording file';\n\n/**\n * Command's handler function\n * @type {Function}\n */\nmodule.exports.handler = command;\n\n/**\n * Builder\n * \n * @param {Object} yargs\n */\nmodule.exports.builder = function(yargs) {\n\n  // Define the recordingFile argument\n  yargs.positional('recordingFile', {\n    describe: 'the recording file',\n    type: 'string',\n    coerce: di.utility.loadYAML\n  });\n\n};\n"
  },
  {
    "path": "commands/init.js",
    "content": "/**\n * Init\n * Create a global config directory for Terminalizer\n *\n * - Create a global config directory\n *   - For Windows, create it under `APPDATA`\n *   - For Linux and MacOS, create it under the home directory \n * - Copy the default config into it\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Executed after the command completes its task\n */\nfunction done() {\n\n  console.log(di.chalk.green('The global config directory is created at'));\n  console.log(di.chalk.magenta(di.utility.getGlobalDirectory()));\n\n}\n\n/**\n * The command's main function\n * \n * @param {Object} argv\n */\nfunction command(argv) {\n\n  var globalPath = di.utility.getGlobalDirectory();\n\n  // Create the global directory\n  try {\n\n    di.fs.mkdirSync(di.utility.getGlobalDirectory(), { recursive: true });\n\n  } catch (error) {\n\n    // Ignore `already exists` error\n    if (error.code != 'EEXIST') {\n      throw error;\n    }\n\n  }\n\n  // Copy the default config file\n  di.fs.copySync(di.path.join(ROOT_PATH, 'config.yml'), \n                 di.path.join(globalPath, 'config.yml'),\n                 {overwrite: true});\n\n  done();\n\n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = 'init';\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = 'Create a global config directory';\n\n/**\n * Command's handler function\n * @type {Function}\n */\nmodule.exports.handler = command;\n"
  },
  {
    "path": "commands/play.js",
    "content": "/**\n * Play\n * Play a recording file on your terminal\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Print the passed content\n * \n * @param {String}   content\n * @param {Function} callback\n */\nfunction playCallback(content, callback) {\n\n  process.stdout.write(content);\n  callback();\n\n}\n\n/**\n * Executed after the command completes its task\n */\nfunction done() {\n\n  // Full reset for the terminal\n  process.stdout.write('\\033c');\n  process.exit();\n\n}\n\n/**\n * The command's main function\n * \n * @param {Object} argv\n */\nfunction command(argv) {\n\n  process.stdin.pause();\n\n  // Playing options\n  var options = {\n    frameDelay: argv.recordingFile.json.config.frameDelay,\n    maxIdleTime: argv.recordingFile.json.config.maxIdleTime\n  };\n\n  // Use the actual delays between frames as recorded\n  if (argv.realTiming) {\n\n    options = {\n      frameDelay: 'auto',\n      maxIdleTime: 'auto'\n    };\n\n  }\n\n  // When app is closing\n  di.death(done);\n\n  // Add the speedFactor option\n  options.speedFactor = argv.speedFactor;\n\n  // Adjust frames delays\n  adjustFramesDelays(argv.recordingFile.json.records, options);\n\n  // Play the recording records\n  play(argv.recordingFile.json.records, playCallback, null, options);\n\n}\n\n/**\n * Adjust frames delays\n * \n * Options:\n * \n * - frameDelay (default: auto)\n *   - Delay between frames in ms\n *   - If the value is `auto` use the actual recording delays\n *   \n * - maxIdleTime (default: 2000)\n *   - Maximum delay between frames in ms\n *   - Ignored if the `frameDelay` isn't set to `auto`\n *   - Set to `auto` to prevent limiting the max idle time\n * \n * - speedFactor (default: 1)\n *   - Multiply the frames delays by this factor\n * \n * @param {Array}  records\n * @param {Object} options (optional)\n */\nfunction adjustFramesDelays(records, options) {\n\n  // Default value for options\n  if (typeof options === 'undefined') {\n    options = {};\n  }\n\n  // Default value for options.frameDelay\n  if (typeof options.frameDelay === 'undefined') {\n    options.frameDelay = 'auto';\n  }\n\n  // Default value for options.maxIdleTime\n  if (typeof options.maxIdleTime === 'undefined') {\n    options.maxIdleTime = 2000;\n  }\n\n  // Default value for options.speedFactor\n  if (typeof options.speedFactor === 'undefined') {\n    options.speedFactor = 1;\n  }\n\n  // Foreach record\n  records.forEach(function(record) {\n\n    // Adjust the delay according to the options\n    if (options.frameDelay != 'auto') {\n      record.delay = options.frameDelay;\n    } else if (options.maxIdleTime != 'auto' && record.delay > options.maxIdleTime) {\n      record.delay = options.maxIdleTime;\n    }\n\n    // Apply speedFactor\n    record.delay = record.delay * options.speedFactor;\n    \n  });\n\n}\n\n/**\n * Play recording records\n * \n * @param {Array}         records\n * @param {Function}      playCallback\n * @param {Function|Null} doneCallback\n */\nfunction play(records, playCallback, doneCallback) {\n\n  var tasks = [];\n\n  // Default value for doneCallback\n  if (typeof doneCallback === 'undefined') {\n    doneCallback = null;\n  }\n\n  // Foreach record\n  records.forEach(function(record) {\n\n    tasks.push(function(callback) {\n\n      setTimeout(function() {\n        playCallback(record.content, callback);\n      }, record.delay);\n      \n    });\n    \n  });\n\n  di.async.series(tasks, function(error, results) {\n\n    if (doneCallback) {\n      doneCallback();\n    }\n\n  });\n  \n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = 'play <recordingFile>';\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = 'Play a recording file on your terminal';\n\n/**\n * Command's handler function\n * @type {Function}\n */\nmodule.exports.handler = command;\n\n/**\n * Builder\n * \n * @param {Object} yargs\n */\nmodule.exports.builder = function(yargs) {\n\n  // Define the recordingFile argument\n  yargs.positional('recordingFile', {\n    describe: 'The recording file',\n    type: 'string',\n    coerce: di.utility.loadYAML\n  });\n\n  // Define the real-timing option\n  yargs.option('r', {\n    alias: 'real-timing',\n    describe: 'Use the actual delays between frames as recorded',\n    type: 'boolean',\n    default: false\n  });\n\n  // Define the speed-factor option\n  yargs.option('s', {\n    alias: 'speed-factor',\n    describe: 'Speed factor, multiply the frames delays by this factor',\n    type: 'number',\n    default: 1.0\n  });\n\n};\n\n////////////////////////////////////////////////////\n// Module //////////////////////////////////////////\n////////////////////////////////////////////////////\n\n// Play recording records\nmodule.exports.play = play;\n\n// Adjust frames delays\nmodule.exports.adjustFramesDelays = adjustFramesDelays;\n"
  },
  {
    "path": "commands/record.js",
    "content": "/**\n * Record\n * Record your terminal and create a recording file\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * The path of the recording file\n * @type {String}\n */\nvar recordingFile = null;\n\n/**\n * The normalized configurations\n * @type {Object} {json, raw}\n */\nvar config = {};\n\n/**\n * To keep tracking of the timestamp\n * of the last inserted record\n * @type {Number}\n */\nvar lastRecordTimestamp = null;\n\n/**\n * To store the records\n * @type {Array}\n */\nvar records = [];\n\n/**\n * Normalize the config file\n * \n * - Set default values in the json and raw\n * - Change the formatting of the values in the json and raw\n * \n * @param  {Object} config {json, raw}\n * @return {Object} {json, raw}\n */\nfunction normalizeConfig(config) {\n\n  // Default value for command\n  if (!config.json.command) {\n\n    // Windows OS\n    if (di.os.platform() === 'win32') {\n      di.utility.changeYAMLValue(config, 'command', 'powershell.exe');\n    } else {\n      di.utility.changeYAMLValue(config, 'command', 'bash -l');\n    }\n\n  }\n\n  // Default value for cwd\n  if (!config.json.cwd) {\n    di.utility.changeYAMLValue(config, 'cwd', process.cwd());\n  } else {\n    di.utility.changeYAMLValue(config, 'cwd', di.path.resolve(config.json.cwd));\n  }\n\n  // Default value for cols\n  if (isNaN(config.json.cols)) {\n    di.utility.changeYAMLValue(config, 'cols', process.stdout.columns);\n  }\n\n  // Default value for rows\n  if (isNaN(config.json.rows)) {\n    di.utility.changeYAMLValue(config, 'rows', process.stdout.rows);\n  }\n\n  return config;\n\n}\n\n/**\n * Calculate the duration from the last inserted record in ms,\n * and update lastRecordTimestamp\n * \n * @return {Number}\n */\nfunction getDuration() {\n\n  // Calculate the duration from the last inserted record\n  var duration = di.now().toFixed() - lastRecordTimestamp;\n\n  // Update the lastRecordTimestamp\n  lastRecordTimestamp = di.now().toFixed();\n\n  return duration;\n\n}\n\n/**\n * When an input or output is received from the PTY instance\n * \n * @param {Buffer} content\n */\nfunction onData(content) {\n\n  process.stdout.write(content);\n\n  var duration = getDuration();\n\n  if (duration < 5) {\n    var lastRecord = records[records.length - 1];\n    lastRecord.content += content;\n    return;\n  }\n\n  records.push({\n    delay: duration,\n    content: content\n  });\n\n}\n\n/**\n * Executed after the command completes its task\n * Store the output file with reserving the comments\n * \n * @param {Object} argv\n */\nfunction done(argv) {\n\n  var outputYAML = '';\n\n  // Add config parent element\n  outputYAML += '# The configurations that used for the recording, feel free to edit them\\n';\n  outputYAML += 'config:\\n\\n';\n\n  // Add the configurations with indentation\n  outputYAML += config.raw.replace(/^/gm, '  ');\n\n  // Add the records\n  outputYAML += '\\n# Records, feel free to edit them\\n';\n  outputYAML += di.yaml.dump({records: records});\n\n  // Store the data into the recording file\n  try {\n\n    di.fs.writeFileSync(recordingFile, outputYAML, 'utf8');\n\n  } catch (error) {\n\n    return di.errorHandler(error);\n\n  }\n\n  console.log(di.chalk.green('Successfully Recorded'));\n  console.log('The recording data is saved into the file:');\n  console.log(di.chalk.magenta(recordingFile));\n  console.log('You can edit the file and even change the configurations.');\n  console.log(\n    \"The command \" +\n      di.chalk.magenta(\"`terminalizer share`\") +\n      \"can be used anytime to share recordings!\"\n  );\n\n  // Reset STDIN\n  process.stdin.setRawMode(false);\n  process.stdin.pause();\n\n  if (argv.skipSharing) {\n    return\n  }\n\n  di.inquirer.prompt([\n    {\n      type: \"confirm\",\n      name: \"share\",\n      message: \"Would you like to share your recording on www.terminalizer.com?\",\n    },\n  ]).then(function(answers) {\n\n    if (!answers.share) {\n      return;\n    }\n\n    console.log(\n      di.chalk.green(\n        \"Let's now share your recording on https://www.terminalizer.com\"\n      )\n    );\n\n    // Invoke the share command\n    di.commands.share.handler({\n      recordingFile: recordingFile,\n    });\n  });\n\n}\n\n/**\n * The command's main function\n * \n * @param {Object} argv\n */\nfunction command(argv) {\n\n  // Normalize the configurations\n  config = normalizeConfig(argv.config);\n\n  // Store the path of the recordingFile\n  recordingFile = argv.recordingFile;\n\n  // Overwrite the command to be executed\n  if (argv.command) {\n    di.utility.changeYAMLValue(config, 'command', argv.command);\n  }\n\n  // Split the command and its arguments\n  var args = di.stringArgv(config.json.command);\n  var command = args[0];\n  var commandArguments = args.slice(1);\n\n  // PTY instance\n  var ptyProcess = di.pty.spawn(command, commandArguments, {\n    cols: config.json.cols,\n    rows: config.json.rows,\n    cwd: config.json.cwd,\n    env: di.deepmerge(process.env, config.json.env)\n  });\n\n  var onInput = ptyProcess.write.bind(ptyProcess);\n\n  console.log('The recording session is started');\n  console.log('Press', di.chalk.green('CTRL+D'), 'to exit and save the recording');\n\n  // Input and output capturing and redirection\n  process.stdin.on('data', onInput);\n  ptyProcess.on('data', onData);\n  ptyProcess.on('exit', function() {\n    process.stdin.removeListener('data', onInput);\n    done(argv);\n  });\n\n  // Input and output normalization\n  process.stdout.setDefaultEncoding('utf8');\n  process.stdin.setEncoding('utf8');\n  process.stdin.setRawMode(true);\n  process.stdin.resume();\n\n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = 'record <recordingFile>';\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = 'Record your terminal and create a recording file';\n\n/**\n * Handler\n * \n * @param {Object} argv\n */\nmodule.exports.handler = function(argv) {\n\n  // Default value for the config option\n  if (typeof argv.config == 'undefined') {\n    argv.config = di.utility.getDefaultConfig();\n  }\n\n  // Execute the command\n  command(argv);\n  \n};\n\n/**\n * Builder\n * \n * @param {Object} yargs\n */\nmodule.exports.builder = function(yargs) {\n\n  // Define the recordingFile argument\n  yargs.positional('recordingFile', {\n    describe: 'A name for the recording file',\n    type: 'string',\n    coerce: di._.partial(di.utility.resolveFilePath, di._, 'yml')\n  });\n\n  // Define the config option\n  yargs.option('c', {\n    alias: 'config',\n    type: 'string',\n    describe: 'Overwrite the default configurations',\n    requiresArg: true,\n    coerce: di.utility.loadYAML\n  });\n\n  // Define the config option\n  yargs.option('d', {\n    alias: 'command',\n    type: 'string',\n    describe: 'The command to be executed',\n    requiresArg: true,\n    default: null\n  });\n\n  // Define the config option\n  yargs.option('k', {\n    alias: 'skip-sharing',\n    type: 'boolean',\n    describe: 'Skip sharing and showing the sharing prompt message',\n    requiresArg: false,\n    default: false\n  });\n\n  // Add examples\n  yargs.example('$0 record foo', 'Start recording and create a recording file called foo.yml');\n  yargs.example('$0 record foo --config config.yml', 'Start recording with your own configurations');\n\n};\n"
  },
  {
    "path": "commands/render.js",
    "content": "/**\n * Render\n * Render a recording file as an animated gif image\n *\n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nconst tmp = require('tmp');\n\ntmp.setGracefulCleanup();\n\n/**\n * The directory to render the frames into\n */\nvar renderDir = tmp.dirSync({ unsafeCleanup: true }).name;\n\n/**\n * Create a progress bar for processing frames\n *\n * @param  {String}      operation   a name for the operation\n * @param  {Number}      framesCount\n * @return {ProgressBar}\n */\nfunction getProgressBar(operation, framesCount) {\n  return new di.ProgressBar(\n    operation +\n      \" \" +\n      di.chalk.magenta(\"frame :current/:total\") +\n      \" :percent [:bar] :etas\",\n    {\n      width: 30,\n      total: framesCount,\n    }\n  );\n}\n\n/**\n * Write the recording data into render/data.json\n *\n * @param  {Object}  recordingFile\n * @return {Promise}\n */\nfunction writeRecordingData(recordingFile) {\n  return new Promise(function (resolve, reject) {\n    // Write the data into data.json file in the root path of the app\n    di.fs.writeFile(\n      di.path.join(ROOT_PATH, \"render/data.json\"),\n      JSON.stringify(recordingFile.json),\n      \"utf8\",\n      function (error) {\n        if (error) {\n          return reject(error);\n        }\n\n        resolve();\n      }\n    );\n  });\n}\n\n/**\n * Read and parse a PNG image file\n *\n * @param  {String}  path the absolute path of the image\n * @return {Promise} resolve with the parsed PNG image\n */\nfunction loadPNG(path) {\n  return new Promise(function (resolve, reject) {\n    di.fs.readFile(path, function (error, imageData) {\n      if (error) {\n        return reject(error);\n      }\n\n      new di.PNG().parse(imageData, function (error, data) {\n        if (error) {\n          return reject(error);\n        }\n\n        resolve(data);\n      });\n    });\n  });\n}\n\n/**\n * Get the dimensions of the first rendered frame\n *\n * @return {Promise}\n */\nfunction getFrameDimensions() {\n  // The path of the first rendered frame\n  var framePath = di.path.join(renderDir, \"0.png\");\n\n  // Read and parse a PNG image file\n  return loadPNG(framePath).then(function (png) {\n    return {\n      width: png.width,\n      height: png.height,\n    };\n  });\n}\n/**\n * Render the frames into PNG images\n *\n * @param  {Array}   records [{delay, content}, ...]\n * @param  {Object}  options {step}\n * @return {Promise}\n */\nfunction renderFrames(records, options) {\n  return new Promise(function (resolve, reject) {\n    // The number of frames\n    var framesCount = records.length;\n\n    // Track execution time\n    var start = Date.now();\n\n    // Create a progress bar\n    var progressBar = getProgressBar(\n      \"Rendering\",\n      Math.ceil(framesCount / options.step)\n    );\n\n    // Execute the rendering process\n    var render = di.spawn(\n      di.electron,\n      [di.path.join(ROOT_PATH, \"render/index.js\"), renderDir, options.step,],\n      { detached: false }\n    );\n\n    render.stdout.on('data', onData);\n    render.stderr.on('data', onError);\n    render.on('close', onClose); \n\n    // Track progress of rendering through stdout\n    function onData(data) {\n\n      // Is not a recordIndex (to skip Electron's logs or new lines)\n      if (isNaN(parseInt(data.toString()))) {\n        return;\n      }\n\n      progressBar.tick();\n    }\n\n    // Track rendering errors observed on stderr\n    function onError(error) {\n\n      // If error is Buffer, print it, otherwise reject\n      if (!!error && error instanceof Buffer) {\n        console.log(di.chalk.yellow(`[render] ${error.toString('utf8').trim()}`));\n      } else {\n        render.kill();\n        reject(new Error(\"Unknown error [\" + typeof error + \"]: \" + error));\n      }\n    } \n\n    // React when rendering process finishes\n    function onClose(code) {\n      if (code !== 0) {\n        reject(new Error(\"Rendering exited with code \" + code));\n      } else {\n        if (progressBar.complete) {\n          console.log(di.chalk.green('[render] Process successfully completed in ' + (Date.now() - start) + 'ms.'));\n        } else {\n          console.log(di.chalk.yellow('[render] Process completion unverified'));\n        }\n\n        resolve();\n      }\n    };\n  });\n}\n\n/**\n * Merge the rendered frames into an animated GIF image\n *\n * @param  {Array}   records         [{delay, content}, ...]\n * @param  {Object}  options         {quality, repeat, step, outputFile}\n * @param  {Object}  frameDimensions {width, height}\n * @return {Promise}\n */\nfunction mergeFrames(records, options, frameDimensions) {\n  return new Promise(function (resolve, reject) {\n    // The number of frames\n    var framesCount = records.length;\n    \n    // Track execution time\n    var start = Date.now();\n\n    // Used for the step option\n    var stepsCounter = 0;\n\n    // Create a progress bar\n    var progressBar = getProgressBar(\n      \"Merging\",\n      Math.ceil(framesCount / options.step)\n    );\n\n    // The gif image\n    var gif = new di.GIFEncoder(frameDimensions.width, frameDimensions.height, {\n      highWaterMark: 5 * 1024 * 1024,\n    });\n\n    // Pipe\n    gif.pipe(di.fs.createWriteStream(options.outputFile));\n\n    // Quality\n    gif.setQuality(101 - options.quality);\n\n    // Repeat\n    gif.setRepeat(options.repeat);\n\n    // Write the headers\n    gif.writeHeader();\n\n    di.async.eachOfSeries(\n      records,\n      function (frame, index, callback) {\n        if (stepsCounter != 0) {\n          stepsCounter = (stepsCounter + 1) % options.step;\n          return callback();\n        }\n\n        stepsCounter = (stepsCounter + 1) % options.step;\n\n        // The path of the rendered frame\n        var framePath = di.path.join(renderDir, index + \".png\");\n\n        // Read and parse the rendered frame\n        loadPNG(framePath)\n          .then(function (png) {\n            progressBar.tick();\n\n            // Set the duration (the delay of the next frame)\n            // The % is used to take the delay of the first frame\n            // as the duration of the last frame\n            gif.setDelay(records[(index + 1) % framesCount].delay);\n\n            // Add frames\n            gif.addFrame(png.data);\n\n            // Next\n            callback();\n          })\n          .catch(function (error) {\n            callback(error);\n          });\n      },\n      function (error) {\n        if (error) {\n          return reject(error);\n        }\n\n        // Write the footer\n        gif.finish();\n\n        // Finish\n        console.log(di.chalk.green('[merge] Process successfully completed in ' + (Date.now() - start) + 'ms.'));\n        resolve();\n      }\n    );\n  });\n}\n\n/**\n * Delete the temporary rendered PNG images\n *\n * @return {Promise}\n */\nfunction cleanup() {\n  return new Promise(function (resolve, reject) {\n    di.fs.emptyDir(di.path.join(ROOT_PATH, \"render/frames\"), function (error) {\n      if (error) {\n        return reject(error);\n      }\n\n      resolve();\n    });\n  });\n}\n\n/**\n * Executed after the command completes its task\n *\n * @param {String} outputFile the path of the rendered image\n */\nfunction done(outputFile) {\n  console.log(\"\\n\" + di.chalk.green(\"Successfully Rendered\"));\n  console.log(\"The animated GIF image is saved into the file:\");\n  console.log(di.chalk.magenta(outputFile));\n  process.exit();\n}\n\n/**\n * The command's main function\n *\n * @param {Object} argv\n */\nfunction command(argv) {\n  // Frames\n  var records = argv.recordingFile.json.records;\n  var config = argv.recordingFile.json.config;\n\n  // Number of frames in the recording file\n  var framesCount = records.length;\n\n  // The path of the output file\n  var outputFile = di.utility.resolveFilePath(\n    \"render\" + Date.now(),\n    \"gif\"\n  );\n\n  // For adjusting (calculating) the frames delays\n  var adjustFramesDelaysOptions = {\n    frameDelay: config.frameDelay,\n    maxIdleTime: config.maxIdleTime,\n  };\n\n  // For rendering the frames into PNG images\n  var renderingOptions = {\n    step: argv.step,\n  };\n\n  // For merging the rendered frames into an animated GIF image\n  var mergingOptions = {\n    quality: config.quality,\n    repeat: config.repeat,\n    step: argv.step,\n    outputFile: outputFile,\n  };\n\n  // Overwrite the quality of the rendered image\n  if (argv.quality) {\n    mergingOptions.quality = argv.quality;\n  }\n\n  // Overwrite the outputFile of the rendered image\n  if (argv.output) {\n    outputFile = argv.output;\n    mergingOptions.outputFile = argv.output;\n  }\n\n  // Tasks\n  di.asyncPromises\n    .waterfall([\n      // Remove all previously rendered frames\n      cleanup,\n\n      // Write the recording data into render/data.json\n      di._.partial(writeRecordingData, argv.recordingFile),\n\n      // Render the frames into PNG images\n      di._.partial(renderFrames, records, renderingOptions),\n\n      // Adjust frames delays\n      di._.partial(\n        di.commands.play.adjustFramesDelays,\n        records,\n        adjustFramesDelaysOptions\n      ),\n\n      // Get the dimensions of the first rendered frame\n      di._.partial(getFrameDimensions),\n\n      // Merge the rendered frames into an animated GIF image\n      di._.partial(mergeFrames, records, mergingOptions),\n\n      // Delete the temporary rendered PNG images\n      cleanup,\n    ])\n    .then(function () {\n      done(outputFile);\n    })\n    .catch(di.errorHandler);\n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = \"render <recordingFile>\";\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = \"Render a recording file as an animated gif image\";\n\n/**\n * Command's handler function\n * @type {Function}\n */\nmodule.exports.handler = command;\n\n/**\n * Builder\n *\n * @param {Object} yargs\n */\nmodule.exports.builder = function (yargs) {\n  // Define the recordingFile argument\n  yargs.positional(\"recordingFile\", {\n    describe: \"The recording file\",\n    type: \"string\",\n    coerce: di.utility.loadYAML,\n  });\n\n  // Define the output option\n  yargs.option(\"o\", {\n    alias: \"output\",\n    type: \"string\",\n    describe: \"A name for the output file\",\n    requiresArg: true,\n    coerce: di._.partial(di.utility.resolveFilePath, di._, \"gif\"),\n  });\n\n  // Define the quality option\n  yargs.option(\"q\", {\n    alias: \"quality\",\n    type: \"number\",\n    describe: \"The quality of the rendered image (1 - 100)\",\n    requiresArg: true,\n  });\n\n  // Define the quality option\n  yargs.option(\"s\", {\n    alias: \"step\",\n    type: \"number\",\n    describe: \"To reduce the number of rendered frames (step > 1)\",\n    requiresArg: true,\n    default: 1,\n  });\n};\n"
  },
  {
    "path": "commands/share.js",
    "content": "/**\n * Share\n * Upload a recording file and get a link for an online player\n *\n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Executed after the command completes its task\n *\n * @param {String} url the url of the uploaded recording\n */\nfunction done(url) {\n  console.log(di.chalk.green('Successfully Uploaded'));\n  console.log('The recording is available on the link:');\n  console.log(di.chalk.magenta(url));\n  process.exit();\n}\n\n/**\n * Check if the value is not an empty value\n *\n * - Throw `Required field` if empty\n *\n * @param  {String} input\n * @return {Boolean}\n */\nfunction isSet(input) {\n  if (!input) {\n    return new Error('Required field');\n  }\n\n  return true;\n}\n\n/**\n * Get a token for uploading recordings\n *\n * - Check if already registered\n *   - Yes: use the token\n *   - No: Generate a new one and ask the user to register it\n *\n * @return {Promise}\n */\nfunction getToken(context) {\n  var token = di.utility.getToken();\n\n  // Already registered\n  if (token) {\n    return Promise.resolve(token);\n  }\n\n  token = di.utility.generateToken();\n\n  console.log('Open the following link in your browser and login into your account');\n  console.log(di.chalk.dim(BASEURL + '/token?token=' + token) + '\\n');\n\n  // Continue action\n  return new Promise(function (resolve, reject) {\n    console.log('When you do it, press any key to continue');\n    process.stdin.setRawMode(true);\n    process.stdin.resume();\n\n    process.stdin.once('data', function handler(data) {\n      // Check if CTRL+C is pressed to exit\n      if (data == '\\u0003' || data == '\\u0003') {\n        process.exit();\n      }\n\n      console.log(di.chalk.dim('Enjoy !') + '\\n');\n      process.stdin.pause();\n      process.stdin.setRawMode(false);\n\n      resolve(token);\n    });\n  });\n}\n\n/**\n * Ask the user to enter meta data about the recording\n *\n * - Skip the task if already executed before\n *   and resolve with the last result\n *\n * @param  {Object}  context\n * @return {Promise}\n */\nfunction getMeta(context) {\n  var platform = di.utility.getOS();\n\n  // Already executed\n  if (typeof context.getMeta != 'undefined') {\n    return Promise.resolve(context.getMeta);\n  }\n\n  console.log('Please enter some details about your recording');\n\n  return di.inquirer\n    .prompt([\n      {\n        type: 'input',\n        name: 'title',\n        message: 'Title',\n        validate: isSet,\n      },\n      {\n        type: 'input',\n        name: 'description',\n        message: 'Description',\n        validate: isSet,\n      },\n      {\n        type: 'input',\n        name: 'tags',\n        message: 'Tags ' + di.chalk.dim('such as git,bash,game'),\n        validate: isSet,\n        default: platform,\n      },\n    ])\n    .then(function (answers) {\n      var params = Object.assign({}, answers);\n\n      // Add the platform\n      params.platform = platform;\n\n      // Print a new line\n      console.log();\n\n      return params;\n    });\n}\n\n/**\n * Upload the recording\n *\n * - If the token is rejected\n *   - Delete the token\n *   - Jump into the getToken task\n *\n * - Resolve with the url of the uploaded recording\n *\n * @param  {Object}  context\n * @return {Promise}\n */\nfunction shareRecording(context) {\n  var self = this;\n  var token = context.getToken;\n  var meta = context.getMeta;\n  var recordingFile = context.recordingFile;\n\n  var options = {\n    method: 'POST',\n    url: BASEURL + '/v1/recording',\n    formData: {\n      title: meta.title,\n      description: meta.description,\n      tags: meta.tags,\n      platform: meta.platform,\n      token: token,\n      file: {\n        value: di.fs.createReadStream(recordingFile),\n        options: {\n          filename: 'recording.yml',\n          contentType: 'application/x-yaml',\n        },\n      },\n    },\n  };\n\n  return di\n    .axios(options)\n    .then((response) => {\n      // Internal server error\n      if (response.status === 500) {\n        throw new Error(response.data.errors.join('\\n'));\n      }\n\n      // Invalid input\n      if (response.status === 400) {\n        throw new Error(response.data.errors.join('\\n'));\n      }\n\n      // Invalid token\n      if (response.status === 401) {\n        di.utility.removeToken();\n        self.jump('getToken');\n        return;\n      }\n\n      // Unexpected error\n      if (response.status !== 200) {\n        throw new Error('Something went wrong, try again later');\n      }\n\n      // Resolve with the URL from the response body\n      return response.data.url;\n    })\n    .catch((error) => {\n      // Reject the promise with the error\n      throw error;\n    });\n}\n\n/**\n * The command's main function\n *\n * @param {Object} argv\n */\nfunction command(argv) {\n  // No global config\n  if (!di.utility.isGlobalDirectoryCreated()) {\n    require('./init.js').handler();\n  }\n\n  var context = { ...argv };\n\n  // Get a token for uploading recordings\n  getToken(context)\n    .then(function (token) {\n      context.getToken = token;\n      // Ask the user to enter meta data about the recording\n      return getMeta(context);\n    })\n    .then(function (meta) {\n      context.getMeta = meta;\n      // Upload the recording\n      return shareRecording(context);\n    })\n    .then(function (url) {\n      done(url);\n    })\n    .catch(di.errorHandler);\n}\n\n////////////////////////////////////////////////////\n// Command Definition //////////////////////////////\n////////////////////////////////////////////////////\n\n/**\n * Command's usage\n * @type {String}\n */\nmodule.exports.command = 'share <recordingFile>';\n\n/**\n * Command's description\n * @type {String}\n */\nmodule.exports.describe = 'Upload a recording file and get a link for an online player';\n\n/**\n * Command's handler function\n * @type {Function}\n */\nmodule.exports.handler = command;\n\n/**\n * Builder\n *\n * @param {Object} yargs\n */\nmodule.exports.builder = function (yargs) {\n  // Define the recordingFile argument\n  yargs.positional('recordingFile', {\n    describe: 'the recording file',\n    type: 'string',\n    coerce: di._.partial(di.utility.resolveFilePath, di._, 'yml'),\n  });\n};\n"
  },
  {
    "path": "config.yml",
    "content": "# Specify a command to be executed\n# like `/bin/bash -l`, `ls`, or any other commands\n# the default is bash for Linux\n# or powershell.exe for Windows\ncommand: null\n\n# Specify the current working directory path\n# the default is the current working directory path\ncwd: null\n\n# Export additional ENV variables\nenv:\n  recording: true\n\n# Explicitly set the number of columns\n# or use `auto` to take the current\n# number of columns of your shell\ncols: auto\n\n# Explicitly set the number of rows\n# or use `auto` to take the current\n# number of rows of your shell\nrows: auto\n\n# Amount of times to repeat GIF\n# If value is -1, play once\n# If value is 0, loop indefinitely\n# If value is a positive number, loop n times\nrepeat: 0\n\n# Quality\n# 1 - 100\nquality: 100\n\n# Delay between frames in ms\n# If the value is `auto` use the actual recording delays\nframeDelay: auto\n\n# Maximum delay between frames in ms\n# Ignored if the `frameDelay` isn't set to `auto`\n# Set to `auto` to prevent limiting the max idle time\nmaxIdleTime: 2000\n\n# The surrounding frame box\n# The `type` can be null, window, floating, or solid`\n# To hide the title use the value null\n# Don't forget to add a backgroundColor style with a null as type\nframeBox:\n  type: floating\n  title: Terminalizer\n  style:\n    border: 0px black solid\n    # boxShadow: none\n    # margin: 0px\n\n# Add a watermark image to the rendered gif\n# You need to specify an absolute path for\n# the image on your machine or a URL, and you can also\n# add your own CSS styles\nwatermark:\n  imagePath: null\n  style:\n    position: absolute\n    right: 15px\n    bottom: 15px\n    width: 100px\n    opacity: 0.9\n\n# Cursor style can be one of\n# `block`, `underline`, or `bar`\ncursorStyle: block\n\n# Font family\n# You can use any font that is installed on your machine\n# in CSS-like syntax\nfontFamily: \"Monaco, Lucida Console, Ubuntu Mono, Monospace\"\n\n# The size of the font\nfontSize: 12\n\n# The height of lines\nlineHeight: 1\n\n# The spacing between letters\nletterSpacing: 0\n\n# Theme\ntheme:\n  background: \"transparent\"\n  foreground: \"#afafaf\"\n  cursor: \"#c7c7c7\"\n  black: \"#232628\"\n  red: \"#fc4384\"\n  green: \"#b3e33b\"\n  yellow: \"#ffa727\"\n  blue: \"#75dff2\"\n  magenta: \"#ae89fe\"\n  cyan: \"#708387\"\n  white: \"#d5d5d0\"\n  brightBlack: \"#626566\"\n  brightRed: \"#ff7fac\"\n  brightGreen: \"#c8ed71\"\n  brightYellow: \"#ebdf86\"\n  brightBlue: \"#75dff2\"\n  brightMagenta: \"#ae89fe\"\n  brightCyan: \"#b1c6ca\"\n  brightWhite: \"#f9f9f4\"\n"
  },
  {
    "path": "di.js",
    "content": "/**\n * Dependency injection\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nvar path = require('path'),\n    _ = require('lodash');\n\n/**\n * Dependency injection\n */\nfunction DI() {\n\n  /**\n   * Injected dependecies\n   * @type {Object}\n   */\n  this._dependencies = {};\n\n  /**\n   * A wrapper proxy object to set traps\n   * @type {Proxy}\n   */\n  this._proxy = new Proxy(this, {\n    get: this.getHandler,\n    set: this.setHandler\n  });\n\n  return this._proxy;\n\n}\n\n/**\n * Trap for getting a property value\n * \n * @param  {Object} target\n * @param  {String} key\n * @return {*}\n */\nDI.prototype.getHandler = function(target, key) {\n\n  if (key in target) {\n    return target[key];\n  }\n\n  if (key in target._dependencies) {\n    return target._dependencies[key];\n  }\n\n};\n\n/**\n * Trap for setting a property value\n * \n * @param  {Object} target\n * @param  {String} key\n * @param  {*}      value\n * @return {*}\n */\nDI.prototype.setHandler = function(target, key, value) {\n\n  if (key in target) {\n    throw new Error(`It is not allowed to set '${key}'`);\n  }\n\n  target._dependencies[key] = value;\n\n  return true;\n\n};\n\n/**\n * Require and set a package\n *\n * - Require the module\n * - Add the module as depndency\n * - Format the key as\n *   - Convert the moduleName to camelCase\n *   - Remove the extension\n * - Resolve third party packages as the native `require`.\n * - Resolve our own scripts with paths relative to\n *   the app's root path `require`.\n * \n * @param {String} moduleName\n * @param {String} key        (Optional) (Default: the moduleName camel cased)\n */\nDI.prototype.require = function(moduleName, key) {\n\n  var parsedModuleName = path.parse(moduleName);\n\n  // Default value for key\n  if (typeof key == 'undefined') {\n    key = _.camelCase(parsedModuleName.name);\n  }\n\n  // Is not a third party package\n  if (parsedModuleName.dir != '') {\n\n    // Resolve the path to an absolute path\n    moduleName = path.resolve(this._getAppRootPath(), moduleName);\n\n  }\n\n  this._dependencies[key] = require(moduleName);\n\n};\n\n/**\n * Inject a dependency\n * \n * @param {String} key\n * @param {*}      value\n */\nDI.prototype.set = function(key, value) {\n\n  this[key] = value;\n  \n};\n\n/**\n * Get an injected dependency\n * \n * @param  {String} key\n * @return {*}\n */\nDI.prototype.get = function(key) {\n\n  return this[key];\n  \n};\n\n/**\n * Get the root path of the app\n *\n * - Follow the module.parent.parent... etc until null\n * \n * @return {String}\n */\nDI.prototype._getAppRootPath = function() {\n\n  var parent = module.parent;\n\n  while (parent.parent) {\n\n    parent = parent.parent;\n    \n  }\n\n  return path.dirname(parent.filename);\n\n};\n\nmodule.exports = DI;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"terminalizer\",\n  \"version\": \"0.12.0\",\n  \"description\": \"Record your terminal and generate animated gif images or share a web player\",\n  \"main\": \"bin/app.js\",\n  \"author\": \"Mohammad Fares <faressoft.com@gmail.com>\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://www.terminalizer.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/faressoft/terminalizer.git\"\n  },\n  \"bin\": {\n    \"terminalizer\": \"bin/app.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"NODE_ENV=development webpack --watch\",\n    \"build\": \"NODE_ENV=production webpack --progress\",\n    \"prepublish\": \"npm run build\"\n  },\n  \"keywords\": [\n    \"terminal\",\n    \"record\",\n    \"capture\",\n    \"tty\",\n    \"shot\",\n    \"bash\",\n    \"powershell\",\n    \"gif\",\n    \"animated\",\n    \"generate\",\n    \"theme\",\n    \"colors\",\n    \"font\",\n    \"repeat\",\n    \"command-line\",\n    \"shell\",\n    \"zsh\",\n    \"bash-profile\",\n    \"render\",\n    \"pty\"\n  ],\n  \"dependencies\": {\n    \"@homebridge/node-pty-prebuilt-multiarch\": \"^0.11.14\",\n    \"async\": \"^2.6.3\",\n    \"async-promises\": \"^0.2.2\",\n    \"axios\": \"^1.7.5\",\n    \"chalk\": \"^2.4.2\",\n    \"death\": \"^1.1.0\",\n    \"deepmerge\": \"^2.2.1\",\n    \"electron\": \"^25.2.0\",\n    \"fs-extra\": \"^5.0.0\",\n    \"gif-encoder\": \"^0.6.1\",\n    \"inquirer\": \"^6.5.2\",\n    \"js-yaml\": \"^3.13.1\",\n    \"lodash\": \"^4.17.15\",\n    \"performance-now\": \"^2.1.0\",\n    \"pngjs\": \"^3.4.0\",\n    \"progress\": \"^2.0.3\",\n    \"require-dir\": \"^1.1.0\",\n    \"string-argv\": \"0.0.2\",\n    \"tmp\": \"^0.2.1\",\n    \"uuid\": \"^10.0.0\",\n    \"yargs\": \"^17.7.2\"\n  },\n  \"devDependencies\": {\n    \"ajv\": \"^6.12.6\",\n    \"clean-webpack-plugin\": \"^4.0.0\",\n    \"css-loader\": \"^4.3.0\",\n    \"jquery\": \"^3.4.1\",\n    \"mini-css-extract-plugin\": \"^2.7.6\",\n    \"terminalizer-player\": \"^0.4.1\",\n    \"webpack\": \"^5.88.1\",\n    \"webpack-cli\": \"^4.10.0\",\n    \"xterm\": \"^v3.14.5\"\n  }\n}\n"
  },
  {
    "path": "render/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>Renderer</title>\n  <meta charset=\"UTF-8\">\n\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"dist/css/app.css\">\n  <script src=\"dist/js/app.js\" type=\"text/javascript\"></script>\n</head>\n<body>\n  \n  <div id=\"terminal\"></div>\n\n</body>\n</html>\n"
  },
  {
    "path": "render/index.js",
    "content": "/**\n * Render the frames into PNG images\n * An electron app, takes one command line argument `step`\n *\n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { app } = require('electron');\nconst { BrowserWindow } = require('electron');\nconst ipcMain = require('electron').ipcMain;\nconst os = require('os');\n\nlet mainWindow = null;\n\n/**\n * The directory to render the frames into\n * @type {String}\n */\nconst renderDir = process.argv[2];\n\n/**\n * The step option\n * To reduce the number of rendered frames (step > 1)\n * @type {Number}\n */\nconst step = process.argv[3] || 1;\n\n// Hide the Dock for macOS\nif (os.platform() == 'darwin') {\n  app.dock.hide();\n}\n\n// When the app is ready\napp.on('ready', createWindow);\n\n/**\n * Create a hidden browser window and load the rendering page\n */\nfunction createWindow() {\n  // Create a browser window\n  mainWindow = new BrowserWindow({\n    show: false,\n    width: 8000,\n    height: 8000,\n    webPreferences: {\n      preload: path.join(__dirname, 'preload.js'),\n    },\n  });\n\n  // Load index.html\n  mainWindow.loadURL('file://' + __dirname + '/index.html');\n}\n\n/**\n * A callback function for the event:\n * getOptions to request the options that need\n * to be passed to the renderer\n *\n * @param {Object} event\n */\nipcMain.handle('getOptions', function () {\n  return { step };\n});\n\n/**\n * A callback function for the event:\n * capturePage\n *\n * @param {Object} event\n */\nipcMain.handle('capturePage', async function (event, captureRect, frameIndex) {\n  // To show the cursor for headless browser\n  mainWindow.focusOnWebView();\n  const img = await mainWindow.webContents.capturePage(captureRect);\n  const outputPath = path.join(renderDir, frameIndex + '.png');\n  fs.writeFileSync(outputPath, img.toPNG());\n  console.log(frameIndex);\n});\n\n/**\n * A callback function for the event:\n * Close\n *\n * @param {Object} event\n * @param {String} error\n */\nipcMain.on('close', function (event, error) {\n  mainWindow.close();\n});\n\n/**\n * A callback function for the event:\n * When something unexpected happened\n *\n * @param {Object} event\n * @param {String} error\n */\nipcMain.on('error', function (event, error) {\n  process.stderr.write(error);\n});\n"
  },
  {
    "path": "render/preload.js",
    "content": "const { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('app', {\n  close() {\n    return ipcRenderer.send('close');\n  },\n  getOptions() {\n    return ipcRenderer.invoke('getOptions');\n  },\n  capturePage(captureRect, frameIndex) {\n    return ipcRenderer.invoke('capturePage', captureRect, frameIndex);\n  },\n});\n\n// Catch all unhandled errors\nwindow.onerror = function (error) {\n  ipcRenderer.send('error', error);\n};\n"
  },
  {
    "path": "render/src/css/app.css",
    "content": "/**\n * Terminalizer\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nbody {\n  background-color: white;\n  margin: 0;\n}\n\n#terminal {\n  display: inline-block;\n  font-size: 0px;\n}\n"
  },
  {
    "path": "render/src/js/app.js",
    "content": "/**\n * Terminalizer\n *\n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nimport async from 'async';\nimport 'terminalizer-player';\n\n// Styles\nimport '../css/app.css';\nimport 'terminalizer-player/dist/css/terminalizer.min.css';\nimport 'xterm/dist/xterm.css';\n\n/**\n * Used for the step option\n * @type {Number}\n */\nvar stepsCounter = 0;\n\n/**\n * Rendering options\n */\nvar options = {};\n\n/**\n * A callback function for the event:\n * When the document is loaded\n */\n$(document).ready(async () => {\n  options = await app.getOptions();\n\n  // Initialize the terminalizer plugin\n  $('#terminal').terminalizer({\n    recordingFile: 'data.json',\n    autoplay: true,\n    controls: false,\n  });\n\n  /**\n   * A callback function for the event:\n   * When the terminal playing is started\n   */\n  $('#terminal').one('playingStarted', function () {\n    var terminalizer = $('#terminal').data('terminalizer');\n\n    // Pause the playing\n    terminalizer.pause();\n  });\n\n  /**\n   * A callback function for the event:\n   * When the terminal playing is paused\n   */\n  $('#terminal').one('playingPaused', function () {\n    var terminalizer = $('#terminal').data('terminalizer');\n\n    // Reset the terminal\n    terminalizer._terminal.reset();\n\n    // When the terminal's reset is done\n    $('#terminal').one('rendered', render);\n  });\n});\n\n/**\n * Render each frame and capture it\n */\nfunction render() {\n  var terminalizer = $('#terminal').data('terminalizer');\n  var framesCount = terminalizer.getFramesCount();\n\n  // Foreach frame\n  async.timesSeries(\n    framesCount,\n    function (frameIndex, next) {\n      terminalizer._renderFrame(frameIndex, true, function () {\n        capture(frameIndex, next);\n      });\n    },\n    function (error) {\n      if (error) {\n        throw new Error(error);\n      }\n\n      app.close();\n    }\n  );\n}\n\n/**\n * Capture the current frame\n *\n * @param {Number}   frameIndex\n * @param {Function} callback\n */\nfunction capture(frameIndex, callback) {\n  var width = $('#terminal').width();\n  var height = $('#terminal').height();\n  var captureRect = { x: 0, y: 0, width: width, height: height };\n\n  if (stepsCounter != 0) {\n    stepsCounter = (stepsCounter + 1) % options.step;\n    return callback();\n  }\n\n  stepsCounter = (stepsCounter + 1) % options.step;\n\n  app\n    .capturePage(captureRect, frameIndex)\n    .then(callback)\n    .catch((err) => {\n      throw err;\n    });\n}\n"
  },
  {
    "path": "utility.js",
    "content": "/**\n * Provide utility functions\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Check if a path represents a valid path for a file\n * \n * @param  {String}  filePath an absolute or a relative path\n * @return {Boolean}\n */\nfunction isFile(filePath) {\n\n  // Resolve the path into an absolute path\n  filePath = di.path.resolve(filePath);\n\n  try {\n\n    return di.fs.statSync(filePath).isFile();\n\n  } catch (error) {\n\n    return false;\n\n  }\n\n}\n\n/**\n * Check if a path represents a valid path for a directory\n * \n * @param  {String}  dirPath an absolute or a relative path\n * @return {Boolean}\n */\nfunction isDir(dirPath) {\n\n  // Resolve the path into an absolute path\n  dirPath = di.path.resolve(dirPath);\n\n  try {\n\n    return di.fs.statSync(dirPath).isDirectory();\n\n  } catch (error) {\n\n    return false;\n\n  }\n\n}\n\n/**\n * Load a file's content\n *\n * - Check if the file exists, if not found check\n *   if the file exists with appending the extension\n *\n * Throws\n * - The provided file doesn't exit\n * - Any reading errors\n * \n * @param  {String} filePath  an absolute or a relative path\n * @param  {String} extension\n * @return {String}\n */\nfunction loadFile(filePath, extension) {\n\n  var content = null;\n\n  // Resolve the path into an absolute path\n  filePath = resolveFilePath(filePath, extension);\n\n  // The file doesn't exist\n  if (!isFile(filePath)) {\n    throw new Error('The provided file doesn\\'t exit');\n  }\n\n  // Read the file\n  try {\n    content = di.fs.readFileSync(filePath);\n  } catch (error) {\n    throw new Error(error);\n  }\n\n  return content;\n\n}\n\n/**\n * Check, load, and parse YAML files\n *\n * - Add .yml extension when needed\n * \n * Throws\n * - The provided file doesn't exit\n * - The provided file is not a valid YAML file\n * - Any reading errors\n * \n * @param  {String} filePath an absolute or a relative path\n * @return {Object}\n */\nfunction loadYAML(filePath) {\n\n  var file = loadFile(filePath, 'yml');\n\n  // Parse the file\n  try {\n\n    return {\n      json: di.yaml.load(file),\n      raw: file.toString()\n    };\n\n  } catch (error) {\n\n    throw new Error('The provided file is not a valid YAML file');\n\n  }\n\n}\n\n/**\n * Check, load, and parse JSON files\n *\n * - Add .json extension when needed\n * \n * Throws\n * - The provided file doesn't exit\n * - The provided file is not a valid JSON file\n * - Any reading errors\n * \n * @param  {String} filePath an absolute or a relative path\n * @return {Object}\n */\nfunction loadJSON(filePath) {\n\n  var file = loadFile(filePath, 'json');\n\n  // Read the file\n  try {\n    file = di.fs.readFileSync(filePath);\n  } catch (error) {\n    throw new Error(error);\n  }\n\n  // Parse the file\n  try {\n    return JSON.parse(file);\n  } catch (error) {\n    throw new Error('The provided file is not a valid JSON file');\n  }\n\n}\n\n/**\n * Resolve to an absolute path\n *\n * Accepts\n *   - FileName\n *   - FileName.ext\n *   - /path/to/FileName\n *   - /path/to/FileName.ext\n *\n * - Add the extension if not already added\n * - Resolve to `/path/to/FileName.ext`\n * \n * @param  {String} filePath  an absolute or a relative path\n * @param  {String} extension\n * @return {String}\n */\nfunction resolveFilePath(filePath, extension) {\n\n  var resolvedPath = di.path.resolve(filePath);\n\n  // The extension is not added\n  if (di.path.extname(resolvedPath) != '.' + extension) {\n    resolvedPath += '.' + extension;\n  }\n\n  return resolvedPath;\n\n}\n\n/**\n * Get the default configurations\n *\n * - Check if there is a global config file\n *   - Found: Get the global config file\n *   - Not Found: Get the default config file\n * \n * @return {Object} {json, raw}\n */\nfunction getDefaultConfig() {\n\n  var defaultConfigPath = di.path.join(ROOT_PATH, 'config.yml');\n  var globalConfigPath = di.path.join(getGlobalDirectory(), 'config.yml');\n\n  // The global config file exists\n  if (isFile(globalConfigPath)) {\n    return loadYAML(globalConfigPath);\n  }\n\n  console.log('defaultConfigPath');\n\n  // Load global config file\n  return loadYAML(defaultConfigPath);\n\n}\n\n/**\n * Change a value for a specific key in YAML\n * \n * - Works only with the first level keys\n * - Works only with keys with a single value\n * - Apply the changes on the json and raw\n *\n * @param {Object} data  {json, raw} \n * @param {String} key\n * @param {*}      value\n */\nfunction changeYAMLValue(data, key, value) {\n\n  data.json[key] = value;\n  data.raw = data.raw.replace(new RegExp('^' + key + ':.+$', 'm'), key + ': ' + value);\n\n}\n\n/**\n * Get the path of the global config directory\n *\n * - For Windows, get the path of APPDATA\n * - For Linux and MacOS, get the path of the home directory \n * \n * @return {String}\n */\nfunction getGlobalDirectory() {\n  // Windows\n  if (typeof process.env.APPDATA != 'undefined') {\n    return di.path.join(process.env.APPDATA, 'terminalizer');\n  }\n\n  return di.path.join(process.env.HOME, '.config/terminalizer');\n}\n\n/**\n * Check if the global config directory is created\n * \n * @return {Boolean}\n */\nfunction isGlobalDirectoryCreated() {\n\n  var globalDirPath = getGlobalDirectory();\n\n  return isDir(globalDirPath);\n\n}\n\n/**\n * Generate and save a token to be used for uploading recordings\n * \n * @param  {String} token\n * @return {String}\n */\nfunction generateToken(token) {\n\n  var token = di.uuid.v4();\n  var globalDirPath = getGlobalDirectory();\n  var tokenPath = di.path.join(globalDirPath, 'token.txt');\n\n  di.fs.writeFileSync(tokenPath, token, 'utf8');\n\n  return token;\n\n}\n\n/**\n * Get registered token for uploading recordings\n * \n * @return {String|Null}\n */\nfunction getToken() {\n\n  var globalDirPath = getGlobalDirectory();\n  var tokenPath = di.path.join(globalDirPath, 'token.txt');\n\n  // The file doesn't exist\n  if (!isFile(tokenPath)) {\n    return null;\n  }\n\n  return di.fs.readFileSync(tokenPath, 'utf8');\n\n}\n\n/**\n * Remove a registered token\n */\nfunction removeToken() {\n\n  var globalDirPath = getGlobalDirectory();\n  var tokenPath = di.path.join(globalDirPath, 'token.txt');\n\n  // The file doesn't exist\n  if (!isFile(tokenPath)) {\n    return;\n  }\n\n  di.fs.unlinkSync(tokenPath);\n\n}\n\n/**\n * Get the name of the current OS\n * \n * @return {String} mac, windows, linux\n */\nfunction getOS() {\n\n  // MacOS\n  if (di.os.platform() == 'darwin') {\n\n    return 'mac';\n\n  // Windows\n  } else if (di.os.platform() == 'win32') {\n\n    return 'windows';\n\n  }\n\n  return 'linux';\n\n}\n\n////////////////////////////////////////////////////\n// Module //////////////////////////////////////////\n////////////////////////////////////////////////////\n\nmodule.exports = {\n  loadYAML: loadYAML,\n  loadJSON: loadJSON,\n  resolveFilePath: resolveFilePath,\n  getDefaultConfig: getDefaultConfig,\n  changeYAMLValue: changeYAMLValue,\n  getGlobalDirectory: getGlobalDirectory,\n  isGlobalDirectoryCreated: isGlobalDirectoryCreated,\n  generateToken: generateToken,\n  getToken: getToken,\n  removeToken: removeToken,\n  getOS: getOS\n};\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const webpack = require('webpack');\nconst path = require('path');\n\n// Extract CSS into separate files\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\n\n// Global variables\nconst globals = {\n  $: 'jquery',\n  jQuery: 'jquery',\n  Terminal: ['xterm', 'Terminal'],\n  'window.jQuery': 'jquery',\n  'window.$': 'jquery',\n};\n\nmodule.exports = {\n  mode: 'production',\n  target: 'electron-renderer',\n  entry: {\n    app: './render/src/js/app.js',\n  },\n  output: {\n    filename: 'js/[name].js',\n    path: path.resolve(__dirname, 'render/dist'),\n    publicPath: '/dist/',\n  },\n  plugins: [\n    new CleanWebpackPlugin({\n      cleanBeforeEveryBuildPatterns: [path.join(__dirname, 'render/dist')],\n    }),\n    new webpack.ProvidePlugin(globals),\n    new MiniCssExtractPlugin({ filename: 'css/[name].css' }),\n    new webpack.NoEmitOnErrorsPlugin(),\n  ],\n  module: {\n    rules: [\n      // CSS\n      {\n        test: /\\.css$/,\n        use: [{ loader: MiniCssExtractPlugin.loader }, { loader: 'css-loader' }],\n      },\n    ],\n  },\n};\n"
  }
]