Full Code of faressoft/terminalizer for AI

master 7b7da2913987 cached
25 files
70.7 KB
19.0k tokens
53 symbols
1 requests
Download .txt
Repository: faressoft/terminalizer
Branch: master
Commit: 7b7da2913987
Files: 25
Total size: 70.7 KB

Directory structure:
gitextract__ztfea2a/

├── .gitignore
├── .jscsrc
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── app.js
├── bin/
│   └── app.js
├── commands/
│   ├── config.js
│   ├── generate.js
│   ├── init.js
│   ├── play.js
│   ├── record.js
│   ├── render.js
│   └── share.js
├── config.yml
├── di.js
├── package.json
├── render/
│   ├── index.html
│   ├── index.js
│   ├── preload.js
│   └── src/
│       ├── css/
│       │   └── app.css
│       └── js/
│           └── app.js
├── utility.js
└── webpack.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Rendering data
render/frames/*
render/data.json

# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Dependency directories
node_modules

# Optional npm cache directory
.npm

# Build
render/dist

# Private assets
demo
logo

# Others
.DS_Store


================================================
FILE: .jscsrc
================================================
{
  "requireCurlyBraces": [
    "if",
    "else",
    "for",
    "while",
    "do",
    "try",
    "catch"
  ],
  "requireSpaceAfterKeywords": [
    "if",
    "else",
    "for",
    "while",
    "do",
    "switch",
    "return",
    "try",
    "catch"
  ],
  "requireSemicolons": true,
  "requireSpacesInForStatement": true,
  "requireSpaceBeforeBlockStatements": true,
  "requireParenthesesAroundIIFE": true,
  "requireSpacesInConditionalExpression": true,
  "requireSpacesInAnonymousFunctionExpression": {
    "beforeOpeningCurlyBrace": true
  },
  "requireSpacesInNamedFunctionExpression": {
    "beforeOpeningCurlyBrace": true
  },
  "requireBlocksOnNewline": true,
  "disallowEmptyBlocks": false,
  "disallowSpacesInsideObjectBrackets": true,
  "disallowSpacesInsideArrayBrackets": true,
  "disallowSpacesInsideParentheses": true,
  "requireSpaceAfterComma": true,
  "disallowSpaceAfterPrefixUnaryOperators": [
    "++",
    "--",
    "+",
    "-",
    "~",
    "!"
  ],
  "disallowSpaceBeforePostfixUnaryOperators": [
    "++",
    "--"
  ],
  "requireSpaceBeforeBinaryOperators": [
    "=",
    "+=",
    "-=",
    "*=",
    "/=",
    "%=",
    "<<=",
    ">>=",
    ">>>=",
    "&=",
    "|=",
    "^=",
    "+",
    "-",
    "*",
    "/",
    "%",
    "<<",
    ">>",
    ">>>",
    "&",
    "|",
    "^",
    "&&",
    "||",
    "===",
    "==",
    ">=",
    "<=",
    "<",
    ">",
    "!=",
    "!=="
  ],
  "requireSpaceAfterBinaryOperators": true,
  "requireCamelCaseOrUpperCaseIdentifiers": {
    "ignoreProperties": true
  },
  "disallowKeywords": [
    "with"
  ],
  "disallowMultipleLineStrings": true,
  "validateLineBreaks": "LF",
  "validateIndentation": 2,
  "disallowTrailingComma": true,
  "requireLineFeedAtFileEnd": true,
  "validateQuoteMarks": {
    "mark": "'",
    "escape": true
  },
  "requireCapitalizedComments": true,
  "requireSpaceAfterLineComment": {
    "allExcept": [
      "//////////////////////////////////////////////////"
    ]
  },
  "jsDoc": {
    "checkAnnotations": true,
    "checkRedundantAccess": true,
    "checkTypes": "capitalizedNativeCase",
    "requireNewlineAfterDescription": true,
    "checkParamExistence": true,
    "checkParamNames": true,
    "requireParamTypes": true,
    "checkRedundantParams": true,
    "requireReturnTypes": true
  }
}


================================================
FILE: .npmignore
================================================
# Rendering data
render/frames/*
render/data.json

# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Dependency directories
node_modules

# Optional npm cache directory
.npm

# Whitelist build
!dist


================================================
FILE: .prettierrc
================================================
{
  "semi": true,
  "singleQuote": true,
  "printWidth": 120
}

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Mohammad Fares

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
  <a href="https://www.terminalizer.com">
    <img src="/img/logo.png?raw=true" width="200"/>
  </a>
</p>

# Terminalizer

[![npm](https://img.shields.io/npm/v/terminalizer.svg)](https://www.npmjs.com/package/terminalizer)
[![npm](https://img.shields.io/npm/l/terminalizer.svg)](https://github.com/faressoft/terminalizer/blob/master/LICENSE)
[![Gitter](https://badges.gitter.im/join_chat.svg)](https://gitter.im/terminalizer/Lobby)
[![Unicorn](https://img.shields.io/badge/nyancat-approved-ff69b4.svg)](https://www.youtube.com/watch?v=QH2-TGUlwu4)
[![Tweet](https://img.shields.io/badge/twitter-share-76abec.svg)](https://goo.gl/QJzJu1)

> Record your terminal and generate animated gif images or share a web player link [www.terminalizer.com](https://www.terminalizer.com)

<p align="center"><img src="/img/demo.gif?raw=true"/></p>

Built to be jusT cOol 👌🦄 !

> If you think so, support me with a `star` and a `follow` 😘

---

<p align="center"><img src="/img/trending.png?raw=true"/></p>

---

# Table of Contents

- [Terminalizer](#terminalizer)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [What's Next](#whats-next)
- [Installation](#installation)
- [Getting Started](#getting-started)
  - [Compression](#compression)
- [Usage](#usage)
  - [Init](#init)
  - [Config](#config)
  - [Record](#record)
  - [Play](#play)
  - [Render](#render)
  - [Share](#share)
  - [Generate](#generate)
- [Configurations](#configurations)
  - [Recording](#recording)
  - [Delays](#delays)
  - [GIF](#gif)
  - [Terminal](#terminal)
  - [Theme](#theme)
  - [Watermark](#watermark)
  - [Frame Box](#frame-box)
    - [Null Frame](#null-frame)
    - [Window Frame](#window-frame)
    - [Floating Frame](#floating-frame)
    - [Solid Frame](#solid-frame)
    - [Solid Frame Without Title](#solid-frame-without-title)
    - [Styling Hint](#styling-hint)
- [FAQ](#faq)
  - [How to support ZSH](#how-to-support-zsh)
- [Issues](#issues)
- [License](#license)

# Features

- Highly customizable.
- Cross platform (Linux, Windows, MacOS).
- Custom `window frames`.
- Custom `font`.
- Custom `colors`.
- Custom `styles` with `CSS`.
- Watermark.
- Edit frames and adjust delays before rendering.
- Skipping frames by a step value to reduce the number of rendered frames.
- Render images with texts on them instead of capturing your screen for better quality.
- The ability to configure:
  - The command to capture (bash, powershell.exe, yourOwnCommand, etc)
  - The current working directory.
  - Explicit values for the number of cols and rows.
  - GIF quality and repeating.
  - Frames delays.
  - The max idle time between frames.
  - Cursor style.
  - Font.
  - Font size.
  - Line height.
  - Letter spacing.
  - Theme.

# What's Next

- The `Generate` command to generate a web player for a recording file.
- Support `apt-get`, `yum`, `brew` installation.

# Installation

You need to install [Node.js](https://nodejs.org/en/download/) first, then install the tool globally using this command:

```bash
yarn global add terminalizer
```

<p align="center"><img src="/img/install.gif?raw=true"/></p>

> Still facing an issue? Check the [Issues](#issues) section or open a new issue.

The 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).

# Getting Started

Start recording your terminal using the `record` command.

```bash
terminalizer record demo
```

A 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.

```bash
terminalizer play demo
```

Now let's render our recording as an animated gif.

```bash
terminalizer render demo
```

## Compression

GIF compression is not implemented yet. For now we recommend [https://gifcompressor.com](https://gifcompressor.com).

# Usage

> You can use the `--help` option to get more details about the commands and their options

```bash
terminalizer <command> [options]
```

## Init

> Create a global config directory

```bash
terminalizer init
```

## Config

> Generate a config file in the current directory

```bash
terminalizer config
```

## Record

> Record your terminal and create a recording file

```bash
terminalizer record <recordingFile>
```

Options

```
-c, --config        Overwrite the default configurations                                  [string]
-d, --command       The command to be executed                            [string] [default: null]
-k, --skip-sharing  Skip sharing and showing the sharing prompt message [boolean] [default: false]
```

Examples

```
terminalizer record foo                      Start recording and create a recording file called foo.yml
terminalizer record foo --config config.yml  Start recording with your own configurations
```

## Play

> Play a recording file on your terminal

```bash
terminalizer play <recordingFile>
```

Options

```
-r, --real-timing   Use the actual delays between frames as recorded        [boolean] [default: false]
-s, --speed-factor  Speed factor, multiply the frames delays by this factor [number] [default: 1]
```

## Render

> Render a recording file as an animated gif image

```bash
terminalizer render <recordingFile>
```

Options

```
-o, --output   A name for the output file                                      [string]
-q, --quality  The quality of the rendered image (1 - 100)                     [number]
-s, --step     To reduce the number of rendered frames (step > 1) [number] [default: 1]
```

## Share

> Upload a recording file and get a link for an online player

```bash
terminalizer share <recordingFile>
```

## Generate

> Generate a web player for a recording file

```bash
terminalizer generate <recordingFile>
```

# Configurations

The default `config.yml` file is stored under the root directory of the project. Execute the below command to copy it to your current directory.

> Use any editor to edit the copied `config.yml`, then use the `-c` option to override the default one.

```bash
terminalizer config
```

> RECOMMENDED, use the `init` command to create a global config file to be used instead of the default one.

```bash
terminalizer init
```

For Linux and MacOS, the created directory is located under the home directory `~/config/terminalizer`. For Windows, it is located under the `AppData`.

## Recording

- `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`.
- `cwd`: Specify the current working directory path. The default is the current working directory path.
- `env`: Export additional ENV variables, to be read by your scripts when starting the recording.
- `cols`: Explicitly set the number of columns or use `auto` to take the current number of columns of your shell.
- `rows`: Explicitly set the number of rows or use `auto` to take the current number of rows of your shell.

## Delays

- `frameDelay`: The delay between frames in ms. If the value is `auto` use the actual recording delays.
- `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.

## GIF

- `quality`: The quality of the generated GIF image (1 - 100).
- `repeat`: Amount of times to repeat GIF:
  - If value is `-1`, play once.
  - If value is `0`, loop indefinitely.
  - If value is a positive number, loop `n` times.

## Terminal

- `cursorStyle`: Cursor style can be one of `block`, `underline`, or `bar`.
- `fontFamily`: You can use any font that is installed on your machine like `Monaco` or `Lucida Console` (CSS-like list).
- `fontSize`: The size of the font in pixels.
- `lineHeight`: The height of lines in pixels.
- `letterSpacing`: The spacing between letters in pixels.

## Theme

You can set the colors of your terminal using one of the CSS formats:

- Hex: `#FFFFFF`.
- RGB: `rgb(255, 255, 255)`.
- HSL: `hsl(0, 0%, 100%)`.
- Name: `white`, `red`, `blue`.

> You can use the value `transparent` too.

The default colors that are assigned to the terminal colors are:

- background: ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `transparent`
- foreground: ![#afafaf](https://placehold.it/15/afafaf/000000?text=+) `#afafaf`
- cursor: ![#c7c7c7](https://placehold.it/15/c7c7c7/000000?text=+) `#c7c7c7`
- black: ![#232628](https://placehold.it/15/232628/000000?text=+) `#232628`
- red: ![#fc4384](https://placehold.it/15/fc4384/000000?text=+) `#fc4384`
- green: ![#b3e33b](https://placehold.it/15/b3e33b/000000?text=+) `#b3e33b`
- yellow: ![#ffa727](https://placehold.it/15/ffa727/000000?text=+) `#ffa727`
- blue: ![#75dff2](https://placehold.it/15/75dff2/000000?text=+) `#75dff2`
- magenta: ![#ae89fe](https://placehold.it/15/ae89fe/000000?text=+) `#ae89fe`
- cyan: ![#708387](https://placehold.it/15/708387/000000?text=+) `#708387`
- white: ![#d5d5d0](https://placehold.it/15/d5d5d0/000000?text=+) `#d5d5d0`
- brightBlack: ![#626566](https://placehold.it/15/626566/000000?text=+) `#626566`
- brightRed: ![#ff7fac](https://placehold.it/15/ff7fac/000000?text=+) `#ff7fac`
- brightGreen: ![#c8ed71](https://placehold.it/15/c8ed71/000000?text=+) `#c8ed71`
- brightYellow: ![#ebdf86](https://placehold.it/15/ebdf86/000000?text=+) `#ebdf86`
- brightBlue: ![#75dff2](https://placehold.it/15/75dff2/000000?text=+) `#75dff2`
- brightMagenta: ![#ae89fe](https://placehold.it/15/ae89fe/000000?text=+) `#ae89fe`
- brightCyan: ![#b1c6ca](https://placehold.it/15/b1c6ca/000000?text=+) `#b1c6ca`
- brightWhite: ![#f9f9f4](https://placehold.it/15/f9f9f4/000000?text=+) `#f9f9f4`

## Watermark

You can add a watermark logo to your generated GIF images.

<p align="center"><img src="/img/watermark.gif?raw=true"/></p>

```
watermark:
  imagePath: AbsolutePathOrURL
  style:
    position: absolute
    right: 15px
    bottom: 15px
    width: 100px
    opacity: 0.9
```

- `watermark.imagePath`: An absolute path for the image on your machine or a URL.
- `watermark.style`: Apply CSS styles (camelCase) to the watermark image, like resizing it.

## Frame Box

Terminalizer comes with predefined frames that you can use to make your GIF images look cool.

- `frameBox.type`: Can be `null`, `window`, `floating`, or `solid`.
- `frameBox.title`: To display a title for the frame or `null`.
- `frameBox.style`: To apply custom CSS styles or to override the current ones.

### Null Frame

No frame, just your recording.

<p align="center"><img src="/img/frames/null.gif?raw=true"/></p>

> Don't forget to add a `backgroundColor` under `style`.

```
frameBox:
  type: null
  title: null
  style:
    backgroundColor: black
```

### Window Frame

<p align="center"><img src="/img/frames/window.gif?raw=true"/></p>

```
frameBox:
  type: window
  title: Terminalizer
  style: []
```

### Floating Frame

<p align="center"><img src="/img/frames/floating.gif?raw=true"/></p>

```
frameBox:
  type: floating
  title: Terminalizer
  style: []
```

### Solid Frame

<p align="center"><img src="/img/frames/solid.gif?raw=true"/></p>

```
frameBox:
  type: solid
  title: Terminalizer
  style: []
```

### Solid Frame Without Title

<p align="center"><img src="/img/frames/solid_without_title.gif?raw=true"/></p>

```
frameBox:
  type: solid
  title: null
  style: []
```

### Styling Hint

You can disable the default shadows and margins.

<p align="center"><img src="/img/frames/solid_without_title_without_shadows.gif?raw=true"/></p>

```
frameBox:
  type: solid
  title: null
  style:
    boxShadow: none
    margin: 0px
```

# FAQ

### How to support ZSH

The default command that gets recorded for Linux is `bash -l`. You need to change the default command to `zsh`.

- Generate a config file in the current directory

```bash
terminalizer config
```

- Open the generated config file in your preferred editor.
- Change the `command` to `zsh`:

```
command: zsh
```

- You may need to change the font, check the font that is used in your terminal:

```
fontFamily: "Meslo for Powerline, Meslo LG M for Powerline"
```

- Use the `-c` option to override the config file:

```bash
terminalizer record demo -c config.yml
```

# Issues

> error while loading shared libraries: libXss.so.1: cannot open shared object file: No such file or directory

Solution:

```bash
sudo yum install libXScrnSaver
```

> error while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory

Solution:

```bash
sudo apt-get install libgconf-2-4
```

> Error: EACCES: permission denied, access '/usr/local/lib'

Solution:

```bash
sudo mkdir -p /usr/local/lib/node_modules && sudo chown -R $(whoami):$(whoami) /usr/local/lib/node_modules

# then use the install command in the "Installation" section above
yarn global add terminalizer
```

# License

This project is under the MIT license.


================================================
FILE: app.js
================================================
/**
 * Terminalizer
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

var yargs = require('yargs'),
    requireDir = require('require-dir');
var package = require('./package.json'),
    commands = requireDir('./commands'),
    DI = require('./di.js');

// Define the DI as a global object
global.di = new DI();

// Define the the root path of the app as a global constant
global.ROOT_PATH = __dirname;

// The base url of the Terminalizer website
// `www` is necessary due to https://github.com/faressoft/terminalizer/issues/207
global.BASEURL = 'https://www.terminalizer.com';

// Dependency Injection
di.require('chalk');
di.require('async');
di.require('axios');
di.require('death');
di.require('path');
di.require('os');
di.require('electron');
di.require('deepmerge');
di.require('uuid');
di.require('tmp');
di.require('lodash', '_');
di.require('fs-extra', 'fs');
di.require('js-yaml', 'yaml');
di.require('performance-now', 'now');
di.require('async-promises', 'asyncPromises');
di.require('string-argv', 'stringArgv');
di.require('progress', 'ProgressBar');
di.require('gif-encoder', 'GIFEncoder');
di.require('inquirer');

di.set('pty', require('@homebridge/node-pty-prebuilt-multiarch'));
di.set('PNG', require('pngjs').PNG);
di.set('spawn', require('child_process').spawn);
di.set('utility', require('./utility.js'));
di.set('commands', commands);
di.set('errorHandler', errorHandler);

// Initialize yargs
yargs.usage('Usage: $0 <command> [options]')
     // Add link
     .epilogue('For more information, check https://www.terminalizer.com')
     // Set the version number
     .version(package.version)
     // Add aliases for version and help options
     .alias({v: 'version', h: 'help'})
     // Require to pass a command
     .demandCommand(1, 'The command is missing')
     // Strict mode
     .strict()
     // Set width to 90 cols
     .wrap(100)
     // Handle failures
     .fail(errorHandler);

// Load commands
yargs.command(commands.init)
     .command(commands.config)
     .command(commands.record)
     .command(commands.play)
     .command(commands.render)
     .command(commands.share)
     .command(commands.generate)

debugger;

try {

  // Parse the command line arguments
  yargs.parse();

} catch (error) {

  // Print the error
  errorHandler(error);

}

/**
 * Print an error
 * 
 * @param {String|Error} error
 */
function errorHandler(error) {

  error = error.toString();

  console.error('Error: \n  ' + error + '\n');
  console.error('Hint:\n  Use the ' + di.chalk.green('--help') + ' option to get help about the usage');
  process.exit(1);

}


================================================
FILE: bin/app.js
================================================
#!/usr/bin/env node

/**
 * Terminalizer
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

require('../app.js');


================================================
FILE: commands/config.js
================================================
/**
 * Config
 * Generate a config file in the current directory
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * Executed after the command completes its task
 */
function done() {

  console.log(di.chalk.green('Successfully Saved'));
  console.log('The config file is saved into the file:');
  console.log(di.chalk.magenta('config.yml'));

}

/**
 * The command's main function
 * 
 * @param {Object} argv
 */
function command(argv) {

  // Copy the default config file
  di.fs.copySync(di.path.join(ROOT_PATH, 'config.yml'), 'config.yml');

  done();

}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = 'config';

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = 'Generate a config file in the current directory';

/**
 * Command's handler function
 * @type {Function}
 */
module.exports.handler = command;


================================================
FILE: commands/generate.js
================================================
/**
 * Generate
 * Generate a web player for a recording file
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * Executed after the command completes its task
 */
function done() {

  // Terminate the app
  process.exit();

}

/**
 * The command's main function
 * 
 * @param {Object} argv
 */
function command(argv) {

  console.log('This command is not implemented yet. It will be available in the next versions');

}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = 'generate <recordingFile>';

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = 'Generate a web player for a recording file';

/**
 * Command's handler function
 * @type {Function}
 */
module.exports.handler = command;

/**
 * Builder
 * 
 * @param {Object} yargs
 */
module.exports.builder = function(yargs) {

  // Define the recordingFile argument
  yargs.positional('recordingFile', {
    describe: 'the recording file',
    type: 'string',
    coerce: di.utility.loadYAML
  });

};


================================================
FILE: commands/init.js
================================================
/**
 * Init
 * Create a global config directory for Terminalizer
 *
 * - Create a global config directory
 *   - For Windows, create it under `APPDATA`
 *   - For Linux and MacOS, create it under the home directory 
 * - Copy the default config into it
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * Executed after the command completes its task
 */
function done() {

  console.log(di.chalk.green('The global config directory is created at'));
  console.log(di.chalk.magenta(di.utility.getGlobalDirectory()));

}

/**
 * The command's main function
 * 
 * @param {Object} argv
 */
function command(argv) {

  var globalPath = di.utility.getGlobalDirectory();

  // Create the global directory
  try {

    di.fs.mkdirSync(di.utility.getGlobalDirectory(), { recursive: true });

  } catch (error) {

    // Ignore `already exists` error
    if (error.code != 'EEXIST') {
      throw error;
    }

  }

  // Copy the default config file
  di.fs.copySync(di.path.join(ROOT_PATH, 'config.yml'), 
                 di.path.join(globalPath, 'config.yml'),
                 {overwrite: true});

  done();

}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = 'init';

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = 'Create a global config directory';

/**
 * Command's handler function
 * @type {Function}
 */
module.exports.handler = command;


================================================
FILE: commands/play.js
================================================
/**
 * Play
 * Play a recording file on your terminal
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * Print the passed content
 * 
 * @param {String}   content
 * @param {Function} callback
 */
function playCallback(content, callback) {

  process.stdout.write(content);
  callback();

}

/**
 * Executed after the command completes its task
 */
function done() {

  // Full reset for the terminal
  process.stdout.write('\033c');
  process.exit();

}

/**
 * The command's main function
 * 
 * @param {Object} argv
 */
function command(argv) {

  process.stdin.pause();

  // Playing options
  var options = {
    frameDelay: argv.recordingFile.json.config.frameDelay,
    maxIdleTime: argv.recordingFile.json.config.maxIdleTime
  };

  // Use the actual delays between frames as recorded
  if (argv.realTiming) {

    options = {
      frameDelay: 'auto',
      maxIdleTime: 'auto'
    };

  }

  // When app is closing
  di.death(done);

  // Add the speedFactor option
  options.speedFactor = argv.speedFactor;

  // Adjust frames delays
  adjustFramesDelays(argv.recordingFile.json.records, options);

  // Play the recording records
  play(argv.recordingFile.json.records, playCallback, null, options);

}

/**
 * Adjust frames delays
 * 
 * Options:
 * 
 * - frameDelay (default: auto)
 *   - Delay between frames in ms
 *   - If the value is `auto` use the actual recording delays
 *   
 * - maxIdleTime (default: 2000)
 *   - 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
 * 
 * - speedFactor (default: 1)
 *   - Multiply the frames delays by this factor
 * 
 * @param {Array}  records
 * @param {Object} options (optional)
 */
function adjustFramesDelays(records, options) {

  // Default value for options
  if (typeof options === 'undefined') {
    options = {};
  }

  // Default value for options.frameDelay
  if (typeof options.frameDelay === 'undefined') {
    options.frameDelay = 'auto';
  }

  // Default value for options.maxIdleTime
  if (typeof options.maxIdleTime === 'undefined') {
    options.maxIdleTime = 2000;
  }

  // Default value for options.speedFactor
  if (typeof options.speedFactor === 'undefined') {
    options.speedFactor = 1;
  }

  // Foreach record
  records.forEach(function(record) {

    // Adjust the delay according to the options
    if (options.frameDelay != 'auto') {
      record.delay = options.frameDelay;
    } else if (options.maxIdleTime != 'auto' && record.delay > options.maxIdleTime) {
      record.delay = options.maxIdleTime;
    }

    // Apply speedFactor
    record.delay = record.delay * options.speedFactor;
    
  });

}

/**
 * Play recording records
 * 
 * @param {Array}         records
 * @param {Function}      playCallback
 * @param {Function|Null} doneCallback
 */
function play(records, playCallback, doneCallback) {

  var tasks = [];

  // Default value for doneCallback
  if (typeof doneCallback === 'undefined') {
    doneCallback = null;
  }

  // Foreach record
  records.forEach(function(record) {

    tasks.push(function(callback) {

      setTimeout(function() {
        playCallback(record.content, callback);
      }, record.delay);
      
    });
    
  });

  di.async.series(tasks, function(error, results) {

    if (doneCallback) {
      doneCallback();
    }

  });
  
}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = 'play <recordingFile>';

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = 'Play a recording file on your terminal';

/**
 * Command's handler function
 * @type {Function}
 */
module.exports.handler = command;

/**
 * Builder
 * 
 * @param {Object} yargs
 */
module.exports.builder = function(yargs) {

  // Define the recordingFile argument
  yargs.positional('recordingFile', {
    describe: 'The recording file',
    type: 'string',
    coerce: di.utility.loadYAML
  });

  // Define the real-timing option
  yargs.option('r', {
    alias: 'real-timing',
    describe: 'Use the actual delays between frames as recorded',
    type: 'boolean',
    default: false
  });

  // Define the speed-factor option
  yargs.option('s', {
    alias: 'speed-factor',
    describe: 'Speed factor, multiply the frames delays by this factor',
    type: 'number',
    default: 1.0
  });

};

////////////////////////////////////////////////////
// Module //////////////////////////////////////////
////////////////////////////////////////////////////

// Play recording records
module.exports.play = play;

// Adjust frames delays
module.exports.adjustFramesDelays = adjustFramesDelays;


================================================
FILE: commands/record.js
================================================
/**
 * Record
 * Record your terminal and create a recording file
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * The path of the recording file
 * @type {String}
 */
var recordingFile = null;

/**
 * The normalized configurations
 * @type {Object} {json, raw}
 */
var config = {};

/**
 * To keep tracking of the timestamp
 * of the last inserted record
 * @type {Number}
 */
var lastRecordTimestamp = null;

/**
 * To store the records
 * @type {Array}
 */
var records = [];

/**
 * Normalize the config file
 * 
 * - Set default values in the json and raw
 * - Change the formatting of the values in the json and raw
 * 
 * @param  {Object} config {json, raw}
 * @return {Object} {json, raw}
 */
function normalizeConfig(config) {

  // Default value for command
  if (!config.json.command) {

    // Windows OS
    if (di.os.platform() === 'win32') {
      di.utility.changeYAMLValue(config, 'command', 'powershell.exe');
    } else {
      di.utility.changeYAMLValue(config, 'command', 'bash -l');
    }

  }

  // Default value for cwd
  if (!config.json.cwd) {
    di.utility.changeYAMLValue(config, 'cwd', process.cwd());
  } else {
    di.utility.changeYAMLValue(config, 'cwd', di.path.resolve(config.json.cwd));
  }

  // Default value for cols
  if (isNaN(config.json.cols)) {
    di.utility.changeYAMLValue(config, 'cols', process.stdout.columns);
  }

  // Default value for rows
  if (isNaN(config.json.rows)) {
    di.utility.changeYAMLValue(config, 'rows', process.stdout.rows);
  }

  return config;

}

/**
 * Calculate the duration from the last inserted record in ms,
 * and update lastRecordTimestamp
 * 
 * @return {Number}
 */
function getDuration() {

  // Calculate the duration from the last inserted record
  var duration = di.now().toFixed() - lastRecordTimestamp;

  // Update the lastRecordTimestamp
  lastRecordTimestamp = di.now().toFixed();

  return duration;

}

/**
 * When an input or output is received from the PTY instance
 * 
 * @param {Buffer} content
 */
function onData(content) {

  process.stdout.write(content);

  var duration = getDuration();

  if (duration < 5) {
    var lastRecord = records[records.length - 1];
    lastRecord.content += content;
    return;
  }

  records.push({
    delay: duration,
    content: content
  });

}

/**
 * Executed after the command completes its task
 * Store the output file with reserving the comments
 * 
 * @param {Object} argv
 */
function done(argv) {

  var outputYAML = '';

  // Add config parent element
  outputYAML += '# The configurations that used for the recording, feel free to edit them\n';
  outputYAML += 'config:\n\n';

  // Add the configurations with indentation
  outputYAML += config.raw.replace(/^/gm, '  ');

  // Add the records
  outputYAML += '\n# Records, feel free to edit them\n';
  outputYAML += di.yaml.dump({records: records});

  // Store the data into the recording file
  try {

    di.fs.writeFileSync(recordingFile, outputYAML, 'utf8');

  } catch (error) {

    return di.errorHandler(error);

  }

  console.log(di.chalk.green('Successfully Recorded'));
  console.log('The recording data is saved into the file:');
  console.log(di.chalk.magenta(recordingFile));
  console.log('You can edit the file and even change the configurations.');
  console.log(
    "The command " +
      di.chalk.magenta("`terminalizer share`") +
      "can be used anytime to share recordings!"
  );

  // Reset STDIN
  process.stdin.setRawMode(false);
  process.stdin.pause();

  if (argv.skipSharing) {
    return
  }

  di.inquirer.prompt([
    {
      type: "confirm",
      name: "share",
      message: "Would you like to share your recording on www.terminalizer.com?",
    },
  ]).then(function(answers) {

    if (!answers.share) {
      return;
    }

    console.log(
      di.chalk.green(
        "Let's now share your recording on https://www.terminalizer.com"
      )
    );

    // Invoke the share command
    di.commands.share.handler({
      recordingFile: recordingFile,
    });
  });

}

/**
 * The command's main function
 * 
 * @param {Object} argv
 */
function command(argv) {

  // Normalize the configurations
  config = normalizeConfig(argv.config);

  // Store the path of the recordingFile
  recordingFile = argv.recordingFile;

  // Overwrite the command to be executed
  if (argv.command) {
    di.utility.changeYAMLValue(config, 'command', argv.command);
  }

  // Split the command and its arguments
  var args = di.stringArgv(config.json.command);
  var command = args[0];
  var commandArguments = args.slice(1);

  // PTY instance
  var ptyProcess = di.pty.spawn(command, commandArguments, {
    cols: config.json.cols,
    rows: config.json.rows,
    cwd: config.json.cwd,
    env: di.deepmerge(process.env, config.json.env)
  });

  var onInput = ptyProcess.write.bind(ptyProcess);

  console.log('The recording session is started');
  console.log('Press', di.chalk.green('CTRL+D'), 'to exit and save the recording');

  // Input and output capturing and redirection
  process.stdin.on('data', onInput);
  ptyProcess.on('data', onData);
  ptyProcess.on('exit', function() {
    process.stdin.removeListener('data', onInput);
    done(argv);
  });

  // Input and output normalization
  process.stdout.setDefaultEncoding('utf8');
  process.stdin.setEncoding('utf8');
  process.stdin.setRawMode(true);
  process.stdin.resume();

}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = 'record <recordingFile>';

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = 'Record your terminal and create a recording file';

/**
 * Handler
 * 
 * @param {Object} argv
 */
module.exports.handler = function(argv) {

  // Default value for the config option
  if (typeof argv.config == 'undefined') {
    argv.config = di.utility.getDefaultConfig();
  }

  // Execute the command
  command(argv);
  
};

/**
 * Builder
 * 
 * @param {Object} yargs
 */
module.exports.builder = function(yargs) {

  // Define the recordingFile argument
  yargs.positional('recordingFile', {
    describe: 'A name for the recording file',
    type: 'string',
    coerce: di._.partial(di.utility.resolveFilePath, di._, 'yml')
  });

  // Define the config option
  yargs.option('c', {
    alias: 'config',
    type: 'string',
    describe: 'Overwrite the default configurations',
    requiresArg: true,
    coerce: di.utility.loadYAML
  });

  // Define the config option
  yargs.option('d', {
    alias: 'command',
    type: 'string',
    describe: 'The command to be executed',
    requiresArg: true,
    default: null
  });

  // Define the config option
  yargs.option('k', {
    alias: 'skip-sharing',
    type: 'boolean',
    describe: 'Skip sharing and showing the sharing prompt message',
    requiresArg: false,
    default: false
  });

  // Add examples
  yargs.example('$0 record foo', 'Start recording and create a recording file called foo.yml');
  yargs.example('$0 record foo --config config.yml', 'Start recording with your own configurations');

};


================================================
FILE: commands/render.js
================================================
/**
 * Render
 * Render a recording file as an animated gif image
 *
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

const tmp = require('tmp');

tmp.setGracefulCleanup();

/**
 * The directory to render the frames into
 */
var renderDir = tmp.dirSync({ unsafeCleanup: true }).name;

/**
 * Create a progress bar for processing frames
 *
 * @param  {String}      operation   a name for the operation
 * @param  {Number}      framesCount
 * @return {ProgressBar}
 */
function getProgressBar(operation, framesCount) {
  return new di.ProgressBar(
    operation +
      " " +
      di.chalk.magenta("frame :current/:total") +
      " :percent [:bar] :etas",
    {
      width: 30,
      total: framesCount,
    }
  );
}

/**
 * Write the recording data into render/data.json
 *
 * @param  {Object}  recordingFile
 * @return {Promise}
 */
function writeRecordingData(recordingFile) {
  return new Promise(function (resolve, reject) {
    // Write the data into data.json file in the root path of the app
    di.fs.writeFile(
      di.path.join(ROOT_PATH, "render/data.json"),
      JSON.stringify(recordingFile.json),
      "utf8",
      function (error) {
        if (error) {
          return reject(error);
        }

        resolve();
      }
    );
  });
}

/**
 * Read and parse a PNG image file
 *
 * @param  {String}  path the absolute path of the image
 * @return {Promise} resolve with the parsed PNG image
 */
function loadPNG(path) {
  return new Promise(function (resolve, reject) {
    di.fs.readFile(path, function (error, imageData) {
      if (error) {
        return reject(error);
      }

      new di.PNG().parse(imageData, function (error, data) {
        if (error) {
          return reject(error);
        }

        resolve(data);
      });
    });
  });
}

/**
 * Get the dimensions of the first rendered frame
 *
 * @return {Promise}
 */
function getFrameDimensions() {
  // The path of the first rendered frame
  var framePath = di.path.join(renderDir, "0.png");

  // Read and parse a PNG image file
  return loadPNG(framePath).then(function (png) {
    return {
      width: png.width,
      height: png.height,
    };
  });
}
/**
 * Render the frames into PNG images
 *
 * @param  {Array}   records [{delay, content}, ...]
 * @param  {Object}  options {step}
 * @return {Promise}
 */
function renderFrames(records, options) {
  return new Promise(function (resolve, reject) {
    // The number of frames
    var framesCount = records.length;

    // Track execution time
    var start = Date.now();

    // Create a progress bar
    var progressBar = getProgressBar(
      "Rendering",
      Math.ceil(framesCount / options.step)
    );

    // Execute the rendering process
    var render = di.spawn(
      di.electron,
      [di.path.join(ROOT_PATH, "render/index.js"), renderDir, options.step,],
      { detached: false }
    );

    render.stdout.on('data', onData);
    render.stderr.on('data', onError);
    render.on('close', onClose); 

    // Track progress of rendering through stdout
    function onData(data) {

      // Is not a recordIndex (to skip Electron's logs or new lines)
      if (isNaN(parseInt(data.toString()))) {
        return;
      }

      progressBar.tick();
    }

    // Track rendering errors observed on stderr
    function onError(error) {

      // If error is Buffer, print it, otherwise reject
      if (!!error && error instanceof Buffer) {
        console.log(di.chalk.yellow(`[render] ${error.toString('utf8').trim()}`));
      } else {
        render.kill();
        reject(new Error("Unknown error [" + typeof error + "]: " + error));
      }
    } 

    // React when rendering process finishes
    function onClose(code) {
      if (code !== 0) {
        reject(new Error("Rendering exited with code " + code));
      } else {
        if (progressBar.complete) {
          console.log(di.chalk.green('[render] Process successfully completed in ' + (Date.now() - start) + 'ms.'));
        } else {
          console.log(di.chalk.yellow('[render] Process completion unverified'));
        }

        resolve();
      }
    };
  });
}

/**
 * Merge the rendered frames into an animated GIF image
 *
 * @param  {Array}   records         [{delay, content}, ...]
 * @param  {Object}  options         {quality, repeat, step, outputFile}
 * @param  {Object}  frameDimensions {width, height}
 * @return {Promise}
 */
function mergeFrames(records, options, frameDimensions) {
  return new Promise(function (resolve, reject) {
    // The number of frames
    var framesCount = records.length;
    
    // Track execution time
    var start = Date.now();

    // Used for the step option
    var stepsCounter = 0;

    // Create a progress bar
    var progressBar = getProgressBar(
      "Merging",
      Math.ceil(framesCount / options.step)
    );

    // The gif image
    var gif = new di.GIFEncoder(frameDimensions.width, frameDimensions.height, {
      highWaterMark: 5 * 1024 * 1024,
    });

    // Pipe
    gif.pipe(di.fs.createWriteStream(options.outputFile));

    // Quality
    gif.setQuality(101 - options.quality);

    // Repeat
    gif.setRepeat(options.repeat);

    // Write the headers
    gif.writeHeader();

    di.async.eachOfSeries(
      records,
      function (frame, index, callback) {
        if (stepsCounter != 0) {
          stepsCounter = (stepsCounter + 1) % options.step;
          return callback();
        }

        stepsCounter = (stepsCounter + 1) % options.step;

        // The path of the rendered frame
        var framePath = di.path.join(renderDir, index + ".png");

        // Read and parse the rendered frame
        loadPNG(framePath)
          .then(function (png) {
            progressBar.tick();

            // Set the duration (the delay of the next frame)
            // The % is used to take the delay of the first frame
            // as the duration of the last frame
            gif.setDelay(records[(index + 1) % framesCount].delay);

            // Add frames
            gif.addFrame(png.data);

            // Next
            callback();
          })
          .catch(function (error) {
            callback(error);
          });
      },
      function (error) {
        if (error) {
          return reject(error);
        }

        // Write the footer
        gif.finish();

        // Finish
        console.log(di.chalk.green('[merge] Process successfully completed in ' + (Date.now() - start) + 'ms.'));
        resolve();
      }
    );
  });
}

/**
 * Delete the temporary rendered PNG images
 *
 * @return {Promise}
 */
function cleanup() {
  return new Promise(function (resolve, reject) {
    di.fs.emptyDir(di.path.join(ROOT_PATH, "render/frames"), function (error) {
      if (error) {
        return reject(error);
      }

      resolve();
    });
  });
}

/**
 * Executed after the command completes its task
 *
 * @param {String} outputFile the path of the rendered image
 */
function done(outputFile) {
  console.log("\n" + di.chalk.green("Successfully Rendered"));
  console.log("The animated GIF image is saved into the file:");
  console.log(di.chalk.magenta(outputFile));
  process.exit();
}

/**
 * The command's main function
 *
 * @param {Object} argv
 */
function command(argv) {
  // Frames
  var records = argv.recordingFile.json.records;
  var config = argv.recordingFile.json.config;

  // Number of frames in the recording file
  var framesCount = records.length;

  // The path of the output file
  var outputFile = di.utility.resolveFilePath(
    "render" + Date.now(),
    "gif"
  );

  // For adjusting (calculating) the frames delays
  var adjustFramesDelaysOptions = {
    frameDelay: config.frameDelay,
    maxIdleTime: config.maxIdleTime,
  };

  // For rendering the frames into PNG images
  var renderingOptions = {
    step: argv.step,
  };

  // For merging the rendered frames into an animated GIF image
  var mergingOptions = {
    quality: config.quality,
    repeat: config.repeat,
    step: argv.step,
    outputFile: outputFile,
  };

  // Overwrite the quality of the rendered image
  if (argv.quality) {
    mergingOptions.quality = argv.quality;
  }

  // Overwrite the outputFile of the rendered image
  if (argv.output) {
    outputFile = argv.output;
    mergingOptions.outputFile = argv.output;
  }

  // Tasks
  di.asyncPromises
    .waterfall([
      // Remove all previously rendered frames
      cleanup,

      // Write the recording data into render/data.json
      di._.partial(writeRecordingData, argv.recordingFile),

      // Render the frames into PNG images
      di._.partial(renderFrames, records, renderingOptions),

      // Adjust frames delays
      di._.partial(
        di.commands.play.adjustFramesDelays,
        records,
        adjustFramesDelaysOptions
      ),

      // Get the dimensions of the first rendered frame
      di._.partial(getFrameDimensions),

      // Merge the rendered frames into an animated GIF image
      di._.partial(mergeFrames, records, mergingOptions),

      // Delete the temporary rendered PNG images
      cleanup,
    ])
    .then(function () {
      done(outputFile);
    })
    .catch(di.errorHandler);
}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = "render <recordingFile>";

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = "Render a recording file as an animated gif image";

/**
 * Command's handler function
 * @type {Function}
 */
module.exports.handler = command;

/**
 * Builder
 *
 * @param {Object} yargs
 */
module.exports.builder = function (yargs) {
  // Define the recordingFile argument
  yargs.positional("recordingFile", {
    describe: "The recording file",
    type: "string",
    coerce: di.utility.loadYAML,
  });

  // Define the output option
  yargs.option("o", {
    alias: "output",
    type: "string",
    describe: "A name for the output file",
    requiresArg: true,
    coerce: di._.partial(di.utility.resolveFilePath, di._, "gif"),
  });

  // Define the quality option
  yargs.option("q", {
    alias: "quality",
    type: "number",
    describe: "The quality of the rendered image (1 - 100)",
    requiresArg: true,
  });

  // Define the quality option
  yargs.option("s", {
    alias: "step",
    type: "number",
    describe: "To reduce the number of rendered frames (step > 1)",
    requiresArg: true,
    default: 1,
  });
};


================================================
FILE: commands/share.js
================================================
/**
 * Share
 * Upload a recording file and get a link for an online player
 *
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * Executed after the command completes its task
 *
 * @param {String} url the url of the uploaded recording
 */
function done(url) {
  console.log(di.chalk.green('Successfully Uploaded'));
  console.log('The recording is available on the link:');
  console.log(di.chalk.magenta(url));
  process.exit();
}

/**
 * Check if the value is not an empty value
 *
 * - Throw `Required field` if empty
 *
 * @param  {String} input
 * @return {Boolean}
 */
function isSet(input) {
  if (!input) {
    return new Error('Required field');
  }

  return true;
}

/**
 * Get a token for uploading recordings
 *
 * - Check if already registered
 *   - Yes: use the token
 *   - No: Generate a new one and ask the user to register it
 *
 * @return {Promise}
 */
function getToken(context) {
  var token = di.utility.getToken();

  // Already registered
  if (token) {
    return Promise.resolve(token);
  }

  token = di.utility.generateToken();

  console.log('Open the following link in your browser and login into your account');
  console.log(di.chalk.dim(BASEURL + '/token?token=' + token) + '\n');

  // Continue action
  return new Promise(function (resolve, reject) {
    console.log('When you do it, press any key to continue');
    process.stdin.setRawMode(true);
    process.stdin.resume();

    process.stdin.once('data', function handler(data) {
      // Check if CTRL+C is pressed to exit
      if (data == '\u0003' || data == '\u0003') {
        process.exit();
      }

      console.log(di.chalk.dim('Enjoy !') + '\n');
      process.stdin.pause();
      process.stdin.setRawMode(false);

      resolve(token);
    });
  });
}

/**
 * Ask the user to enter meta data about the recording
 *
 * - Skip the task if already executed before
 *   and resolve with the last result
 *
 * @param  {Object}  context
 * @return {Promise}
 */
function getMeta(context) {
  var platform = di.utility.getOS();

  // Already executed
  if (typeof context.getMeta != 'undefined') {
    return Promise.resolve(context.getMeta);
  }

  console.log('Please enter some details about your recording');

  return di.inquirer
    .prompt([
      {
        type: 'input',
        name: 'title',
        message: 'Title',
        validate: isSet,
      },
      {
        type: 'input',
        name: 'description',
        message: 'Description',
        validate: isSet,
      },
      {
        type: 'input',
        name: 'tags',
        message: 'Tags ' + di.chalk.dim('such as git,bash,game'),
        validate: isSet,
        default: platform,
      },
    ])
    .then(function (answers) {
      var params = Object.assign({}, answers);

      // Add the platform
      params.platform = platform;

      // Print a new line
      console.log();

      return params;
    });
}

/**
 * Upload the recording
 *
 * - If the token is rejected
 *   - Delete the token
 *   - Jump into the getToken task
 *
 * - Resolve with the url of the uploaded recording
 *
 * @param  {Object}  context
 * @return {Promise}
 */
function shareRecording(context) {
  var self = this;
  var token = context.getToken;
  var meta = context.getMeta;
  var recordingFile = context.recordingFile;

  var options = {
    method: 'POST',
    url: BASEURL + '/v1/recording',
    formData: {
      title: meta.title,
      description: meta.description,
      tags: meta.tags,
      platform: meta.platform,
      token: token,
      file: {
        value: di.fs.createReadStream(recordingFile),
        options: {
          filename: 'recording.yml',
          contentType: 'application/x-yaml',
        },
      },
    },
  };

  return di
    .axios(options)
    .then((response) => {
      // Internal server error
      if (response.status === 500) {
        throw new Error(response.data.errors.join('\n'));
      }

      // Invalid input
      if (response.status === 400) {
        throw new Error(response.data.errors.join('\n'));
      }

      // Invalid token
      if (response.status === 401) {
        di.utility.removeToken();
        self.jump('getToken');
        return;
      }

      // Unexpected error
      if (response.status !== 200) {
        throw new Error('Something went wrong, try again later');
      }

      // Resolve with the URL from the response body
      return response.data.url;
    })
    .catch((error) => {
      // Reject the promise with the error
      throw error;
    });
}

/**
 * The command's main function
 *
 * @param {Object} argv
 */
function command(argv) {
  // No global config
  if (!di.utility.isGlobalDirectoryCreated()) {
    require('./init.js').handler();
  }

  var context = { ...argv };

  // Get a token for uploading recordings
  getToken(context)
    .then(function (token) {
      context.getToken = token;
      // Ask the user to enter meta data about the recording
      return getMeta(context);
    })
    .then(function (meta) {
      context.getMeta = meta;
      // Upload the recording
      return shareRecording(context);
    })
    .then(function (url) {
      done(url);
    })
    .catch(di.errorHandler);
}

////////////////////////////////////////////////////
// Command Definition //////////////////////////////
////////////////////////////////////////////////////

/**
 * Command's usage
 * @type {String}
 */
module.exports.command = 'share <recordingFile>';

/**
 * Command's description
 * @type {String}
 */
module.exports.describe = 'Upload a recording file and get a link for an online player';

/**
 * Command's handler function
 * @type {Function}
 */
module.exports.handler = command;

/**
 * Builder
 *
 * @param {Object} yargs
 */
module.exports.builder = function (yargs) {
  // Define the recordingFile argument
  yargs.positional('recordingFile', {
    describe: 'the recording file',
    type: 'string',
    coerce: di._.partial(di.utility.resolveFilePath, di._, 'yml'),
  });
};


================================================
FILE: config.yml
================================================
# Specify a command to be executed
# like `/bin/bash -l`, `ls`, or any other commands
# the default is bash for Linux
# or powershell.exe for Windows
command: null

# Specify the current working directory path
# the default is the current working directory path
cwd: null

# Export additional ENV variables
env:
  recording: true

# Explicitly set the number of columns
# or use `auto` to take the current
# number of columns of your shell
cols: auto

# Explicitly set the number of rows
# or use `auto` to take the current
# number of rows of your shell
rows: auto

# Amount of times to repeat GIF
# If value is -1, play once
# If value is 0, loop indefinitely
# If value is a positive number, loop n times
repeat: 0

# Quality
# 1 - 100
quality: 100

# Delay between frames in ms
# If the value is `auto` use the actual recording delays
frameDelay: auto

# 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
maxIdleTime: 2000

# The surrounding frame box
# The `type` can be null, window, floating, or solid`
# To hide the title use the value null
# Don't forget to add a backgroundColor style with a null as type
frameBox:
  type: floating
  title: Terminalizer
  style:
    border: 0px black solid
    # boxShadow: none
    # margin: 0px

# Add a watermark image to the rendered gif
# You need to specify an absolute path for
# the image on your machine or a URL, and you can also
# add your own CSS styles
watermark:
  imagePath: null
  style:
    position: absolute
    right: 15px
    bottom: 15px
    width: 100px
    opacity: 0.9

# Cursor style can be one of
# `block`, `underline`, or `bar`
cursorStyle: block

# Font family
# You can use any font that is installed on your machine
# in CSS-like syntax
fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace"

# The size of the font
fontSize: 12

# The height of lines
lineHeight: 1

# The spacing between letters
letterSpacing: 0

# Theme
theme:
  background: "transparent"
  foreground: "#afafaf"
  cursor: "#c7c7c7"
  black: "#232628"
  red: "#fc4384"
  green: "#b3e33b"
  yellow: "#ffa727"
  blue: "#75dff2"
  magenta: "#ae89fe"
  cyan: "#708387"
  white: "#d5d5d0"
  brightBlack: "#626566"
  brightRed: "#ff7fac"
  brightGreen: "#c8ed71"
  brightYellow: "#ebdf86"
  brightBlue: "#75dff2"
  brightMagenta: "#ae89fe"
  brightCyan: "#b1c6ca"
  brightWhite: "#f9f9f4"


================================================
FILE: di.js
================================================
/**
 * Dependency injection
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

var path = require('path'),
    _ = require('lodash');

/**
 * Dependency injection
 */
function DI() {

  /**
   * Injected dependecies
   * @type {Object}
   */
  this._dependencies = {};

  /**
   * A wrapper proxy object to set traps
   * @type {Proxy}
   */
  this._proxy = new Proxy(this, {
    get: this.getHandler,
    set: this.setHandler
  });

  return this._proxy;

}

/**
 * Trap for getting a property value
 * 
 * @param  {Object} target
 * @param  {String} key
 * @return {*}
 */
DI.prototype.getHandler = function(target, key) {

  if (key in target) {
    return target[key];
  }

  if (key in target._dependencies) {
    return target._dependencies[key];
  }

};

/**
 * Trap for setting a property value
 * 
 * @param  {Object} target
 * @param  {String} key
 * @param  {*}      value
 * @return {*}
 */
DI.prototype.setHandler = function(target, key, value) {

  if (key in target) {
    throw new Error(`It is not allowed to set '${key}'`);
  }

  target._dependencies[key] = value;

  return true;

};

/**
 * Require and set a package
 *
 * - Require the module
 * - Add the module as depndency
 * - Format the key as
 *   - Convert the moduleName to camelCase
 *   - Remove the extension
 * - Resolve third party packages as the native `require`.
 * - Resolve our own scripts with paths relative to
 *   the app's root path `require`.
 * 
 * @param {String} moduleName
 * @param {String} key        (Optional) (Default: the moduleName camel cased)
 */
DI.prototype.require = function(moduleName, key) {

  var parsedModuleName = path.parse(moduleName);

  // Default value for key
  if (typeof key == 'undefined') {
    key = _.camelCase(parsedModuleName.name);
  }

  // Is not a third party package
  if (parsedModuleName.dir != '') {

    // Resolve the path to an absolute path
    moduleName = path.resolve(this._getAppRootPath(), moduleName);

  }

  this._dependencies[key] = require(moduleName);

};

/**
 * Inject a dependency
 * 
 * @param {String} key
 * @param {*}      value
 */
DI.prototype.set = function(key, value) {

  this[key] = value;
  
};

/**
 * Get an injected dependency
 * 
 * @param  {String} key
 * @return {*}
 */
DI.prototype.get = function(key) {

  return this[key];
  
};

/**
 * Get the root path of the app
 *
 * - Follow the module.parent.parent... etc until null
 * 
 * @return {String}
 */
DI.prototype._getAppRootPath = function() {

  var parent = module.parent;

  while (parent.parent) {

    parent = parent.parent;
    
  }

  return path.dirname(parent.filename);

};

module.exports = DI;


================================================
FILE: package.json
================================================
{
  "name": "terminalizer",
  "version": "0.12.0",
  "description": "Record your terminal and generate animated gif images or share a web player",
  "main": "bin/app.js",
  "author": "Mohammad Fares <faressoft.com@gmail.com>",
  "license": "MIT",
  "homepage": "https://www.terminalizer.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/faressoft/terminalizer.git"
  },
  "bin": {
    "terminalizer": "bin/app.js"
  },
  "scripts": {
    "dev": "NODE_ENV=development webpack --watch",
    "build": "NODE_ENV=production webpack --progress",
    "prepublish": "npm run build"
  },
  "keywords": [
    "terminal",
    "record",
    "capture",
    "tty",
    "shot",
    "bash",
    "powershell",
    "gif",
    "animated",
    "generate",
    "theme",
    "colors",
    "font",
    "repeat",
    "command-line",
    "shell",
    "zsh",
    "bash-profile",
    "render",
    "pty"
  ],
  "dependencies": {
    "@homebridge/node-pty-prebuilt-multiarch": "^0.11.14",
    "async": "^2.6.3",
    "async-promises": "^0.2.2",
    "axios": "^1.7.5",
    "chalk": "^2.4.2",
    "death": "^1.1.0",
    "deepmerge": "^2.2.1",
    "electron": "^25.2.0",
    "fs-extra": "^5.0.0",
    "gif-encoder": "^0.6.1",
    "inquirer": "^6.5.2",
    "js-yaml": "^3.13.1",
    "lodash": "^4.17.15",
    "performance-now": "^2.1.0",
    "pngjs": "^3.4.0",
    "progress": "^2.0.3",
    "require-dir": "^1.1.0",
    "string-argv": "0.0.2",
    "tmp": "^0.2.1",
    "uuid": "^10.0.0",
    "yargs": "^17.7.2"
  },
  "devDependencies": {
    "ajv": "^6.12.6",
    "clean-webpack-plugin": "^4.0.0",
    "css-loader": "^4.3.0",
    "jquery": "^3.4.1",
    "mini-css-extract-plugin": "^2.7.6",
    "terminalizer-player": "^0.4.1",
    "webpack": "^5.88.1",
    "webpack-cli": "^4.10.0",
    "xterm": "^v3.14.5"
  }
}


================================================
FILE: render/index.html
================================================
<!DOCTYPE html>
<html>
<head>
  <title>Renderer</title>
  <meta charset="UTF-8">

  <link rel="stylesheet" type="text/css" href="dist/css/app.css">
  <script src="dist/js/app.js" type="text/javascript"></script>
</head>
<body>
  
  <div id="terminal"></div>

</body>
</html>


================================================
FILE: render/index.js
================================================
/**
 * Render the frames into PNG images
 * An electron app, takes one command line argument `step`
 *
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

const fs = require('fs');
const path = require('path');
const { app } = require('electron');
const { BrowserWindow } = require('electron');
const ipcMain = require('electron').ipcMain;
const os = require('os');

let mainWindow = null;

/**
 * The directory to render the frames into
 * @type {String}
 */
const renderDir = process.argv[2];

/**
 * The step option
 * To reduce the number of rendered frames (step > 1)
 * @type {Number}
 */
const step = process.argv[3] || 1;

// Hide the Dock for macOS
if (os.platform() == 'darwin') {
  app.dock.hide();
}

// When the app is ready
app.on('ready', createWindow);

/**
 * Create a hidden browser window and load the rendering page
 */
function createWindow() {
  // Create a browser window
  mainWindow = new BrowserWindow({
    show: false,
    width: 8000,
    height: 8000,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  // Load index.html
  mainWindow.loadURL('file://' + __dirname + '/index.html');
}

/**
 * A callback function for the event:
 * getOptions to request the options that need
 * to be passed to the renderer
 *
 * @param {Object} event
 */
ipcMain.handle('getOptions', function () {
  return { step };
});

/**
 * A callback function for the event:
 * capturePage
 *
 * @param {Object} event
 */
ipcMain.handle('capturePage', async function (event, captureRect, frameIndex) {
  // To show the cursor for headless browser
  mainWindow.focusOnWebView();
  const img = await mainWindow.webContents.capturePage(captureRect);
  const outputPath = path.join(renderDir, frameIndex + '.png');
  fs.writeFileSync(outputPath, img.toPNG());
  console.log(frameIndex);
});

/**
 * A callback function for the event:
 * Close
 *
 * @param {Object} event
 * @param {String} error
 */
ipcMain.on('close', function (event, error) {
  mainWindow.close();
});

/**
 * A callback function for the event:
 * When something unexpected happened
 *
 * @param {Object} event
 * @param {String} error
 */
ipcMain.on('error', function (event, error) {
  process.stderr.write(error);
});


================================================
FILE: render/preload.js
================================================
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('app', {
  close() {
    return ipcRenderer.send('close');
  },
  getOptions() {
    return ipcRenderer.invoke('getOptions');
  },
  capturePage(captureRect, frameIndex) {
    return ipcRenderer.invoke('capturePage', captureRect, frameIndex);
  },
});

// Catch all unhandled errors
window.onerror = function (error) {
  ipcRenderer.send('error', error);
};


================================================
FILE: render/src/css/app.css
================================================
/**
 * Terminalizer
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

body {
  background-color: white;
  margin: 0;
}

#terminal {
  display: inline-block;
  font-size: 0px;
}


================================================
FILE: render/src/js/app.js
================================================
/**
 * Terminalizer
 *
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

import async from 'async';
import 'terminalizer-player';

// Styles
import '../css/app.css';
import 'terminalizer-player/dist/css/terminalizer.min.css';
import 'xterm/dist/xterm.css';

/**
 * Used for the step option
 * @type {Number}
 */
var stepsCounter = 0;

/**
 * Rendering options
 */
var options = {};

/**
 * A callback function for the event:
 * When the document is loaded
 */
$(document).ready(async () => {
  options = await app.getOptions();

  // Initialize the terminalizer plugin
  $('#terminal').terminalizer({
    recordingFile: 'data.json',
    autoplay: true,
    controls: false,
  });

  /**
   * A callback function for the event:
   * When the terminal playing is started
   */
  $('#terminal').one('playingStarted', function () {
    var terminalizer = $('#terminal').data('terminalizer');

    // Pause the playing
    terminalizer.pause();
  });

  /**
   * A callback function for the event:
   * When the terminal playing is paused
   */
  $('#terminal').one('playingPaused', function () {
    var terminalizer = $('#terminal').data('terminalizer');

    // Reset the terminal
    terminalizer._terminal.reset();

    // When the terminal's reset is done
    $('#terminal').one('rendered', render);
  });
});

/**
 * Render each frame and capture it
 */
function render() {
  var terminalizer = $('#terminal').data('terminalizer');
  var framesCount = terminalizer.getFramesCount();

  // Foreach frame
  async.timesSeries(
    framesCount,
    function (frameIndex, next) {
      terminalizer._renderFrame(frameIndex, true, function () {
        capture(frameIndex, next);
      });
    },
    function (error) {
      if (error) {
        throw new Error(error);
      }

      app.close();
    }
  );
}

/**
 * Capture the current frame
 *
 * @param {Number}   frameIndex
 * @param {Function} callback
 */
function capture(frameIndex, callback) {
  var width = $('#terminal').width();
  var height = $('#terminal').height();
  var captureRect = { x: 0, y: 0, width: width, height: height };

  if (stepsCounter != 0) {
    stepsCounter = (stepsCounter + 1) % options.step;
    return callback();
  }

  stepsCounter = (stepsCounter + 1) % options.step;

  app
    .capturePage(captureRect, frameIndex)
    .then(callback)
    .catch((err) => {
      throw err;
    });
}


================================================
FILE: utility.js
================================================
/**
 * Provide utility functions
 * 
 * @author Mohammad Fares <faressoft.com@gmail.com>
 */

/**
 * Check if a path represents a valid path for a file
 * 
 * @param  {String}  filePath an absolute or a relative path
 * @return {Boolean}
 */
function isFile(filePath) {

  // Resolve the path into an absolute path
  filePath = di.path.resolve(filePath);

  try {

    return di.fs.statSync(filePath).isFile();

  } catch (error) {

    return false;

  }

}

/**
 * Check if a path represents a valid path for a directory
 * 
 * @param  {String}  dirPath an absolute or a relative path
 * @return {Boolean}
 */
function isDir(dirPath) {

  // Resolve the path into an absolute path
  dirPath = di.path.resolve(dirPath);

  try {

    return di.fs.statSync(dirPath).isDirectory();

  } catch (error) {

    return false;

  }

}

/**
 * Load a file's content
 *
 * - Check if the file exists, if not found check
 *   if the file exists with appending the extension
 *
 * Throws
 * - The provided file doesn't exit
 * - Any reading errors
 * 
 * @param  {String} filePath  an absolute or a relative path
 * @param  {String} extension
 * @return {String}
 */
function loadFile(filePath, extension) {

  var content = null;

  // Resolve the path into an absolute path
  filePath = resolveFilePath(filePath, extension);

  // The file doesn't exist
  if (!isFile(filePath)) {
    throw new Error('The provided file doesn\'t exit');
  }

  // Read the file
  try {
    content = di.fs.readFileSync(filePath);
  } catch (error) {
    throw new Error(error);
  }

  return content;

}

/**
 * Check, load, and parse YAML files
 *
 * - Add .yml extension when needed
 * 
 * Throws
 * - The provided file doesn't exit
 * - The provided file is not a valid YAML file
 * - Any reading errors
 * 
 * @param  {String} filePath an absolute or a relative path
 * @return {Object}
 */
function loadYAML(filePath) {

  var file = loadFile(filePath, 'yml');

  // Parse the file
  try {

    return {
      json: di.yaml.load(file),
      raw: file.toString()
    };

  } catch (error) {

    throw new Error('The provided file is not a valid YAML file');

  }

}

/**
 * Check, load, and parse JSON files
 *
 * - Add .json extension when needed
 * 
 * Throws
 * - The provided file doesn't exit
 * - The provided file is not a valid JSON file
 * - Any reading errors
 * 
 * @param  {String} filePath an absolute or a relative path
 * @return {Object}
 */
function loadJSON(filePath) {

  var file = loadFile(filePath, 'json');

  // Read the file
  try {
    file = di.fs.readFileSync(filePath);
  } catch (error) {
    throw new Error(error);
  }

  // Parse the file
  try {
    return JSON.parse(file);
  } catch (error) {
    throw new Error('The provided file is not a valid JSON file');
  }

}

/**
 * Resolve to an absolute path
 *
 * Accepts
 *   - FileName
 *   - FileName.ext
 *   - /path/to/FileName
 *   - /path/to/FileName.ext
 *
 * - Add the extension if not already added
 * - Resolve to `/path/to/FileName.ext`
 * 
 * @param  {String} filePath  an absolute or a relative path
 * @param  {String} extension
 * @return {String}
 */
function resolveFilePath(filePath, extension) {

  var resolvedPath = di.path.resolve(filePath);

  // The extension is not added
  if (di.path.extname(resolvedPath) != '.' + extension) {
    resolvedPath += '.' + extension;
  }

  return resolvedPath;

}

/**
 * Get the default configurations
 *
 * - Check if there is a global config file
 *   - Found: Get the global config file
 *   - Not Found: Get the default config file
 * 
 * @return {Object} {json, raw}
 */
function getDefaultConfig() {

  var defaultConfigPath = di.path.join(ROOT_PATH, 'config.yml');
  var globalConfigPath = di.path.join(getGlobalDirectory(), 'config.yml');

  // The global config file exists
  if (isFile(globalConfigPath)) {
    return loadYAML(globalConfigPath);
  }

  console.log('defaultConfigPath');

  // Load global config file
  return loadYAML(defaultConfigPath);

}

/**
 * Change a value for a specific key in YAML
 * 
 * - Works only with the first level keys
 * - Works only with keys with a single value
 * - Apply the changes on the json and raw
 *
 * @param {Object} data  {json, raw} 
 * @param {String} key
 * @param {*}      value
 */
function changeYAMLValue(data, key, value) {

  data.json[key] = value;
  data.raw = data.raw.replace(new RegExp('^' + key + ':.+$', 'm'), key + ': ' + value);

}

/**
 * Get the path of the global config directory
 *
 * - For Windows, get the path of APPDATA
 * - For Linux and MacOS, get the path of the home directory 
 * 
 * @return {String}
 */
function getGlobalDirectory() {
  // Windows
  if (typeof process.env.APPDATA != 'undefined') {
    return di.path.join(process.env.APPDATA, 'terminalizer');
  }

  return di.path.join(process.env.HOME, '.config/terminalizer');
}

/**
 * Check if the global config directory is created
 * 
 * @return {Boolean}
 */
function isGlobalDirectoryCreated() {

  var globalDirPath = getGlobalDirectory();

  return isDir(globalDirPath);

}

/**
 * Generate and save a token to be used for uploading recordings
 * 
 * @param  {String} token
 * @return {String}
 */
function generateToken(token) {

  var token = di.uuid.v4();
  var globalDirPath = getGlobalDirectory();
  var tokenPath = di.path.join(globalDirPath, 'token.txt');

  di.fs.writeFileSync(tokenPath, token, 'utf8');

  return token;

}

/**
 * Get registered token for uploading recordings
 * 
 * @return {String|Null}
 */
function getToken() {

  var globalDirPath = getGlobalDirectory();
  var tokenPath = di.path.join(globalDirPath, 'token.txt');

  // The file doesn't exist
  if (!isFile(tokenPath)) {
    return null;
  }

  return di.fs.readFileSync(tokenPath, 'utf8');

}

/**
 * Remove a registered token
 */
function removeToken() {

  var globalDirPath = getGlobalDirectory();
  var tokenPath = di.path.join(globalDirPath, 'token.txt');

  // The file doesn't exist
  if (!isFile(tokenPath)) {
    return;
  }

  di.fs.unlinkSync(tokenPath);

}

/**
 * Get the name of the current OS
 * 
 * @return {String} mac, windows, linux
 */
function getOS() {

  // MacOS
  if (di.os.platform() == 'darwin') {

    return 'mac';

  // Windows
  } else if (di.os.platform() == 'win32') {

    return 'windows';

  }

  return 'linux';

}

////////////////////////////////////////////////////
// Module //////////////////////////////////////////
////////////////////////////////////////////////////

module.exports = {
  loadYAML: loadYAML,
  loadJSON: loadJSON,
  resolveFilePath: resolveFilePath,
  getDefaultConfig: getDefaultConfig,
  changeYAMLValue: changeYAMLValue,
  getGlobalDirectory: getGlobalDirectory,
  isGlobalDirectoryCreated: isGlobalDirectoryCreated,
  generateToken: generateToken,
  getToken: getToken,
  removeToken: removeToken,
  getOS: getOS
};


================================================
FILE: webpack.config.js
================================================
const webpack = require('webpack');
const path = require('path');

// Extract CSS into separate files
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

// Global variables
const globals = {
  $: 'jquery',
  jQuery: 'jquery',
  Terminal: ['xterm', 'Terminal'],
  'window.jQuery': 'jquery',
  'window.$': 'jquery',
};

module.exports = {
  mode: 'production',
  target: 'electron-renderer',
  entry: {
    app: './render/src/js/app.js',
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, 'render/dist'),
    publicPath: '/dist/',
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanBeforeEveryBuildPatterns: [path.join(__dirname, 'render/dist')],
    }),
    new webpack.ProvidePlugin(globals),
    new MiniCssExtractPlugin({ filename: 'css/[name].css' }),
    new webpack.NoEmitOnErrorsPlugin(),
  ],
  module: {
    rules: [
      // CSS
      {
        test: /\.css$/,
        use: [{ loader: MiniCssExtractPlugin.loader }, { loader: 'css-loader' }],
      },
    ],
  },
};
Download .txt
gitextract__ztfea2a/

├── .gitignore
├── .jscsrc
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── app.js
├── bin/
│   └── app.js
├── commands/
│   ├── config.js
│   ├── generate.js
│   ├── init.js
│   ├── play.js
│   ├── record.js
│   ├── render.js
│   └── share.js
├── config.yml
├── di.js
├── package.json
├── render/
│   ├── index.html
│   ├── index.js
│   ├── preload.js
│   └── src/
│       ├── css/
│       │   └── app.css
│       └── js/
│           └── app.js
├── utility.js
└── webpack.config.js
Download .txt
SYMBOL INDEX (53 symbols across 13 files)

FILE: app.js
  function errorHandler (line 96) | function errorHandler(error) {

FILE: commands/config.js
  function done (line 11) | function done() {
  function command (line 24) | function command(argv) {

FILE: commands/generate.js
  function done (line 11) | function done() {
  function command (line 23) | function command(argv) {

FILE: commands/init.js
  function done (line 16) | function done() {
  function command (line 28) | function command(argv) {

FILE: commands/play.js
  function playCallback (line 14) | function playCallback(content, callback) {
  function done (line 24) | function done() {
  function command (line 37) | function command(argv) {
  function adjustFramesDelays (line 91) | function adjustFramesDelays(records, options) {
  function play (line 137) | function play(records, playCallback, doneCallback) {

FILE: commands/record.js
  function normalizeConfig (line 42) | function normalizeConfig(config) {
  function getDuration (line 83) | function getDuration() {
  function onData (line 100) | function onData(content) {
  function done (line 125) | function done(argv) {
  function command (line 200) | function command(argv) {

FILE: commands/render.js
  function getProgressBar (line 24) | function getProgressBar(operation, framesCount) {
  function writeRecordingData (line 43) | function writeRecordingData(recordingFile) {
  function loadPNG (line 67) | function loadPNG(path) {
  function getFrameDimensions (line 90) | function getFrameDimensions() {
  function renderFrames (line 109) | function renderFrames(records, options) {
  function mergeFrames (line 182) | function mergeFrames(records, options, frameDimensions) {
  function cleanup (line 270) | function cleanup() {
  function done (line 287) | function done(outputFile) {
  function command (line 299) | function command(argv) {

FILE: commands/share.js
  function done (line 13) | function done(url) {
  function isSet (line 28) | function isSet(input) {
  function getToken (line 45) | function getToken(context) {
  function getMeta (line 88) | function getMeta(context) {
  function shareRecording (line 145) | function shareRecording(context) {
  function command (line 209) | function command(argv) {

FILE: di.js
  function DI (line 13) | function DI() {

FILE: render/index.js
  function createWindow (line 41) | function createWindow() {

FILE: render/preload.js
  method close (line 4) | close() {
  method getOptions (line 7) | getOptions() {
  method capturePage (line 10) | capturePage(captureRect, frameIndex) {

FILE: render/src/js/app.js
  function render (line 69) | function render() {
  function capture (line 97) | function capture(frameIndex, callback) {

FILE: utility.js
  function isFile (line 13) | function isFile(filePath) {
  function isDir (line 36) | function isDir(dirPath) {
  function loadFile (line 67) | function loadFile(filePath, extension) {
  function loadYAML (line 103) | function loadYAML(filePath) {
  function loadJSON (line 136) | function loadJSON(filePath) {
  function resolveFilePath (line 172) | function resolveFilePath(filePath, extension) {
  function getDefaultConfig (line 194) | function getDefaultConfig() {
  function changeYAMLValue (line 222) | function changeYAMLValue(data, key, value) {
  function getGlobalDirectory (line 237) | function getGlobalDirectory() {
  function isGlobalDirectoryCreated (line 251) | function isGlobalDirectoryCreated() {
  function generateToken (line 265) | function generateToken(token) {
  function getToken (line 282) | function getToken() {
  function removeToken (line 299) | function removeToken() {
  function getOS (line 318) | function getOS() {
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (77K chars).
[
  {
    "path": ".gitignore",
    "chars": 263,
    "preview": "# 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#"
  },
  {
    "path": ".jscsrc",
    "chars": 2307,
    "preview": "{\n  \"requireCurlyBraces\": [\n    \"if\",\n    \"else\",\n    \"for\",\n    \"while\",\n    \"do\",\n    \"try\",\n    \"catch\"\n  ],\n  \"requi"
  },
  {
    "path": ".npmignore",
    "chars": 219,
    "preview": "# 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#"
  },
  {
    "path": ".prettierrc",
    "chars": 62,
    "preview": "{\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"printWidth\": 120\n}"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2018 Mohammad Fares\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 13036,
    "preview": "<p align=\"center\">\n  <a href=\"https://www.terminalizer.com\">\n    <img src=\"/img/logo.png?raw=true\" width=\"200\"/>\n  </a>\n"
  },
  {
    "path": "app.js",
    "chars": 2599,
    "preview": "/**\n * Terminalizer\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nvar yargs = require('yargs'),\n    requi"
  },
  {
    "path": "bin/app.js",
    "chars": 124,
    "preview": "#!/usr/bin/env node\n\n/**\n * Terminalizer\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nrequire('../app.js"
  },
  {
    "path": "commands/config.js",
    "chars": 1039,
    "preview": "/**\n * Config\n * Generate a config file in the current directory\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>"
  },
  {
    "path": "commands/generate.js",
    "chars": 1181,
    "preview": "/**\n * Generate\n * Generate a web player for a recording file\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n *"
  },
  {
    "path": "commands/init.js",
    "chars": 1568,
    "preview": "/**\n * Init\n * Create a global config directory for Terminalizer\n *\n * - Create a global config directory\n *   - For Win"
  },
  {
    "path": "commands/play.js",
    "chars": 4818,
    "preview": "/**\n * Play\n * Play a recording file on your terminal\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n "
  },
  {
    "path": "commands/record.js",
    "chars": 7200,
    "preview": "/**\n * Record\n * Record your terminal and create a recording file\n * \n * @author Mohammad Fares <faressoft.com@gmail.com"
  },
  {
    "path": "commands/render.js",
    "chars": 10533,
    "preview": "/**\n * Render\n * Render a recording file as an animated gif image\n *\n * @author Mohammad Fares <faressoft.com@gmail.com>"
  },
  {
    "path": "commands/share.js",
    "chars": 5986,
    "preview": "/**\n * Share\n * Upload a recording file and get a link for an online player\n *\n * @author Mohammad Fares <faressoft.com@"
  },
  {
    "path": "config.yml",
    "chars": 2423,
    "preview": "# Specify a command to be executed\n# like `/bin/bash -l`, `ls`, or any other commands\n# the default is bash for Linux\n# "
  },
  {
    "path": "di.js",
    "chars": 2650,
    "preview": "/**\n * Dependency injection\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nvar path = require('path'),\n   "
  },
  {
    "path": "package.json",
    "chars": 1806,
    "preview": "{\n  \"name\": \"terminalizer\",\n  \"version\": \"0.12.0\",\n  \"description\": \"Record your terminal and generate animated gif imag"
  },
  {
    "path": "render/index.html",
    "chars": 275,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n  <title>Renderer</title>\n  <meta charset=\"UTF-8\">\n\n  <link rel=\"stylesheet\" type=\"text/cs"
  },
  {
    "path": "render/index.js",
    "chars": 2233,
    "preview": "/**\n * Render the frames into PNG images\n * An electron app, takes one command line argument `step`\n *\n * @author Mohamm"
  },
  {
    "path": "render/preload.js",
    "chars": 451,
    "preview": "const { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('app', {\n  close() {\n    re"
  },
  {
    "path": "render/src/css/app.css",
    "chars": 188,
    "preview": "/**\n * Terminalizer\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nbody {\n  background-color: white;\n  mar"
  },
  {
    "path": "render/src/js/app.js",
    "chars": 2383,
    "preview": "/**\n * Terminalizer\n *\n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\nimport async from 'async';\nimport 'termi"
  },
  {
    "path": "utility.js",
    "chars": 6844,
    "preview": "/**\n * Provide utility functions\n * \n * @author Mohammad Fares <faressoft.com@gmail.com>\n */\n\n/**\n * Check if a path rep"
  },
  {
    "path": "webpack.config.js",
    "chars": 1092,
    "preview": "const webpack = require('webpack');\nconst path = require('path');\n\n// Extract CSS into separate files\nconst MiniCssExtra"
  }
]

About this extraction

This page contains the full source code of the faressoft/terminalizer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (70.7 KB), approximately 19.0k tokens, and a symbol index with 53 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!