first commit
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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
|
||||||
73
.jscsrc
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
369
README.md
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# Terminalizer
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/terminalizer)
|
||||||
|
[](https://github.com/faressoft/terminalizer/blob/master/LICENSE)
|
||||||
|
[](https://gitter.im/terminalizer/Lobby)
|
||||||
|
[](https://www.youtube.com/watch?v=QH2-TGUlwu4)
|
||||||
|
|
||||||
|
> Record your terminal and generate animated gif images
|
||||||
|
|
||||||
|
<p align="center"><img src="/img/demo.gif?raw=true"/></p>
|
||||||
|
|
||||||
|
Built to be jusT cOol 👌🦄 !
|
||||||
|
|
||||||
|
> If you think so, support me by a `start` and a `follow` 😘
|
||||||
|
|
||||||
|
Built while listening to [Ever Felt Pt.1 - Otis McDonald](https://www.youtube.com/watch?v=-BiXhuRq7fU) 🎵 And [Nyan Cat](https://www.youtube.com/watch?v=QH2-TGUlwu4) 😛
|
||||||
|
|
||||||
|
# Table of Contents
|
||||||
|
|
||||||
|
* [Features](#features)
|
||||||
|
* [Installation](#installation)
|
||||||
|
* [Getting Started](#getting-started)
|
||||||
|
* [Compression](#compression)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [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)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Highly customizable.
|
||||||
|
* Corss platform (Linux, Windows, MacOS).
|
||||||
|
* Custom `window frames`.
|
||||||
|
* Custom `font`.
|
||||||
|
* Custom `colors`.
|
||||||
|
* Custom `styles` with `CSS`.
|
||||||
|
* Watermark.
|
||||||
|
* Edit 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.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You need first to install [Node.js](https://nodejs.org/en/download/), then install the tool globally using this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g terminalizer
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center"><img src="/img/install.gif?raw=true"/></p>
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Start recording your terminal using the command `record`.
|
||||||
|
|
||||||
|
```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 recoreded frames. You can replay your recording using the command `play`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terminalizer record demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's render our recording as an animated gif.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terminalizer render demo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compression
|
||||||
|
|
||||||
|
GIF compression is not implementated yet. For now we recommend [https://gifcompressor.com](https://gifcompressor.com).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> You can use the option `--help` to get more details about the commands and their options.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terminalizer <command> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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]
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
terminalizer record foo Start recording and create a recording file called foo.yml
|
||||||
|
terminalizer record foo --config config.yml Start recording with 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 at root directory of the project. Execute the bellow command to copy it to your current directory.
|
||||||
|
|
||||||
|
> Use any editor to edit the copied `config.yml`, then use the option `-c` to overwrite the default one.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terminalizer config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recording
|
||||||
|
|
||||||
|
* `command`: 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`.
|
||||||
|
* `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 start 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 columns 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 prevnt 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`.
|
||||||
|
* `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 the value `transparent` too.
|
||||||
|
|
||||||
|
The default colors that are assigned to the termianl colors are:
|
||||||
|
|
||||||
|
* background: transparent
|
||||||
|
* foreground: <code style="background-color: #afafaf">#afafaf</code>.
|
||||||
|
* cursor: <code style="background-color: #c7c7c7">#c7c7c7</code>.
|
||||||
|
* black: <code style="background-color: #232628;">#232628</code>.
|
||||||
|
* red: <code style="background-color: #fc4384">#fc4384</code>.
|
||||||
|
* green: <code style="background-color: #b3e33b">#b3e33b</code>.
|
||||||
|
* yellow: <code style="background-color: #ffa727">#ffa727</code>.
|
||||||
|
* blue: <code style="background-color: #75dff2">#75dff2</code>.
|
||||||
|
* magenta: <code style="background-color: #ae89fe">#ae89fe</code>.
|
||||||
|
* cyan: <code style="background-color: #708387">#708387</code>.
|
||||||
|
* white: <code style="background-color: #d5d5d0">#d5d5d0</code>.
|
||||||
|
* brightBlack: <code style="background-color: #626566">#626566</code>.
|
||||||
|
* brightRed: <code style="background-color: #ff7fac">#ff7fac</code>.
|
||||||
|
* brightGreen: <code style="background-color: #c8ed71">#c8ed71</code>.
|
||||||
|
* brightYellow: <code style="background-color: #ebdf86">#ebdf86</code>.
|
||||||
|
* brightBlue: <code style="background-color: #75dff2">#75dff2</code>.
|
||||||
|
* brightMagenta: <code style="background-color: #ae89fe">#ae89fe</code>.
|
||||||
|
* brightCyan: <code style="background-color: #b1c6ca">#b1c6ca</code>.
|
||||||
|
* brightWhite: <code style="background-color: #f9f9f4">#f9f9f4</code>.
|
||||||
|
|
||||||
|
## 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 overwrite the current onces.
|
||||||
|
|
||||||
|
### 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 by:
|
||||||
|
|
||||||
|
<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
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This project is under the MIT license.
|
||||||
96
app.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Terminalizer
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
var yargs = require('yargs'),
|
||||||
|
is = require('is_js'),
|
||||||
|
chalk = require('chalk'),
|
||||||
|
_ = require('lodash'),
|
||||||
|
async = require('async'),
|
||||||
|
asyncPromises = require('async-promises'),
|
||||||
|
death = require('death'),
|
||||||
|
stringArgv = require('string-argv'),
|
||||||
|
path = require('path'),
|
||||||
|
ProgressBar = require('progress'),
|
||||||
|
GIFEncoder = require('gif-encoder'),
|
||||||
|
PNG = require('pngjs').PNG,
|
||||||
|
yaml = require('js-yaml'),
|
||||||
|
os = require('os'),
|
||||||
|
spawn = require('child_process').spawn,
|
||||||
|
electron = require('electron'),
|
||||||
|
deepmerge = require('deepmerge'),
|
||||||
|
pty = require('node-pty-prebuilt'),
|
||||||
|
fs = require('fs-extra'),
|
||||||
|
now = require('performance-now');
|
||||||
|
var package = require('./package.json'),
|
||||||
|
utility = require('./utility.js'),
|
||||||
|
di = require('./di.js'),
|
||||||
|
play = require('./commands/play.js');
|
||||||
|
|
||||||
|
// Define the DI as a global object
|
||||||
|
global.di = di;
|
||||||
|
|
||||||
|
// Define the the root path of the app as a global constant
|
||||||
|
global.ROOT_PATH = __dirname;
|
||||||
|
|
||||||
|
// Dependency Injection
|
||||||
|
di.set('is', is);
|
||||||
|
di.set('chalk', chalk);
|
||||||
|
di.set('_', _);
|
||||||
|
di.set('async', async);
|
||||||
|
di.set('asyncPromises', asyncPromises);
|
||||||
|
di.set('death', death);
|
||||||
|
di.set('stringArgv', stringArgv);
|
||||||
|
di.set('path', path);
|
||||||
|
di.set('ProgressBar', ProgressBar);
|
||||||
|
di.set('GIFEncoder', GIFEncoder);
|
||||||
|
di.set('PNG', PNG);
|
||||||
|
di.set('os', os);
|
||||||
|
di.set('spawn', spawn);
|
||||||
|
di.set('electron', electron);
|
||||||
|
di.set('deepmerge', deepmerge);
|
||||||
|
di.set('pty', pty);
|
||||||
|
di.set('fs', fs);
|
||||||
|
di.set('now', now);
|
||||||
|
di.set('fs', fs);
|
||||||
|
di.set('yaml', yaml);
|
||||||
|
di.set('utility', utility);
|
||||||
|
di.set('play', play);
|
||||||
|
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)
|
||||||
|
// Automatically loads the commands
|
||||||
|
.commandDir('commands')
|
||||||
|
// Handle failures
|
||||||
|
.fail(errorHandler);
|
||||||
|
|
||||||
|
// Parse the command line arguments
|
||||||
|
var argv = yargs.parse();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print exceptions
|
||||||
|
*
|
||||||
|
* @param {String} message
|
||||||
|
*/
|
||||||
|
function errorHandler(message) {
|
||||||
|
|
||||||
|
console.error('Error: \n ' + message + '\n');
|
||||||
|
console.error('Hint:\n Use the option ' + chalk.green('--help') + ' to get help about the usage');
|
||||||
|
process.exit(1);
|
||||||
|
|
||||||
|
}
|
||||||
9
bin/app.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminalizer
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('../app.js');
|
||||||
53
commands/config.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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'));
|
||||||
|
|
||||||
|
// Terminate the app
|
||||||
|
process.exit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command's main function
|
||||||
|
*
|
||||||
|
* @param {Object} argv
|
||||||
|
*/
|
||||||
|
function command(argv) {
|
||||||
|
|
||||||
|
di.fs.copy(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;
|
||||||
65
commands/generate.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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 avalible 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
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
231
commands/play.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* 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 optinos
|
||||||
|
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 prevnt 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;
|
||||||
273
commands/record.js
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (di.is.not.number(config.json.cols)) {
|
||||||
|
di.utility.changeYAMLValue(config, 'cols', process.stdout.columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default value for rows
|
||||||
|
if (di.is.not.number(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
|
||||||
|
*/
|
||||||
|
function done() {
|
||||||
|
|
||||||
|
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) {
|
||||||
|
di.errorHandler(error.message);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.');
|
||||||
|
|
||||||
|
// Terminate the app
|
||||||
|
process.exit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, {
|
||||||
|
name: 'xterm-color',
|
||||||
|
cols: config.json.cols,
|
||||||
|
rows: config.json.rows,
|
||||||
|
cwd: config.json.pwd,
|
||||||
|
env: di.deepmerge(process.env, config.json.env)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Input and output capturing and redirection
|
||||||
|
ptyProcess.on('data', onData);
|
||||||
|
ptyProcess.on('exit', done);
|
||||||
|
process.stdin.on('data', ptyProcess.write.bind(ptyProcess));
|
||||||
|
|
||||||
|
// Input and output normalization
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
process.stdout.setDefaultEncoding('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) {
|
||||||
|
|
||||||
|
// The default configurations
|
||||||
|
var defaultConfig = di.utility.getDefaultConfig();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 with your own configurations');
|
||||||
|
|
||||||
|
};
|
||||||
381
commands/render.js
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
/**
|
||||||
|
* Render
|
||||||
|
* Render a recording file as an animated gif image
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dimensions of the first rendered frame
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
function getFrameDimensions() {
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
|
||||||
|
// The path of the first rendered frame
|
||||||
|
var framePath = di.path.join(ROOT_PATH, 'render/frames/0.png');
|
||||||
|
|
||||||
|
// Read and parse the image
|
||||||
|
di.fs.createReadStream(framePath).pipe(new di.PNG()).on('parsed', function() {
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
width: this.width,
|
||||||
|
height: this.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;
|
||||||
|
|
||||||
|
// 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'), options.step], {detached: false});
|
||||||
|
|
||||||
|
render.stderr.on('data', function(error) {
|
||||||
|
render.kill();
|
||||||
|
reject(new Error(di._.trim(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
render.stdout.on('data', function(data) {
|
||||||
|
|
||||||
|
progressBar.tick();
|
||||||
|
|
||||||
|
// Rendering is completed
|
||||||
|
if (progressBar.complete) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// Foreach frame
|
||||||
|
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(ROOT_PATH, 'render/frames', index + '.png');
|
||||||
|
|
||||||
|
// Read and parse the rendered frame
|
||||||
|
di.fs.createReadStream(framePath).pipe(new di.PNG()).on('parsed', function() {
|
||||||
|
|
||||||
|
progressBar.tick();
|
||||||
|
|
||||||
|
// Set delay
|
||||||
|
gif.setDelay(frame.delay);
|
||||||
|
|
||||||
|
// Add frames
|
||||||
|
gif.addFrame(this.data);
|
||||||
|
|
||||||
|
// Next
|
||||||
|
callback();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function(error) {
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the footer
|
||||||
|
gif.finish();
|
||||||
|
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' + (new Date()).getTime(), 'gif');
|
||||||
|
|
||||||
|
// For adjusting (calculating) the frames delays
|
||||||
|
var adjustFramesDelaysOptions = {
|
||||||
|
frameDelay: config.frameDelay,
|
||||||
|
maxIdleTime: config.maxIdleTime
|
||||||
|
};
|
||||||
|
|
||||||
|
// For rendering the frames into PMG 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.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(function(error) {
|
||||||
|
|
||||||
|
di.errorHandler(error.message);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
65
commands/share.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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 avalible in the next versions');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// 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.utility.loadYAML
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
106
config.yml
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# 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 prevnt 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
|
||||||
|
fontFamily: Monaco, Lucida Console
|
||||||
|
|
||||||
|
# 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"
|
||||||
43
di.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Dependency Injection
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
var is = require('is_js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependencies
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
var dependency = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific dependency
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @return {Object|Null} return null if not found
|
||||||
|
*/
|
||||||
|
module.exports.get = function(key) {
|
||||||
|
|
||||||
|
// Not found
|
||||||
|
if (is.not.propertyDefined(dependency, key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependency[key];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set/Add a dependency
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Object} value
|
||||||
|
*/
|
||||||
|
module.exports.set = function(key, value) {
|
||||||
|
|
||||||
|
dependency[key] = value;
|
||||||
|
module.exports[key] = value;
|
||||||
|
|
||||||
|
};
|
||||||
BIN
img/demo.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
img/frames/floating.gif
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
img/frames/null.gif
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
img/frames/solid.gif
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
img/frames/solid_without_title.gif
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
img/frames/solid_without_title_without_shadows.gif
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
img/frames/window.gif
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
img/install.gif
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
img/watermark.gif
Normal file
|
After Width: | Height: | Size: 90 KiB |
222
lib/terminalizer.css
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* Terminalizer Web Plugin
|
||||||
|
* https://terminalizer.com
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
.terminalizer {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer .xterm-viewport {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//// [ Reset ] ////*/
|
||||||
|
|
||||||
|
.terminalizer div {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
font-weight: inherit;
|
||||||
|
font-style: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
font-family: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//// [ Window ] ////*/
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window {
|
||||||
|
-moz-border-radius: 6px;
|
||||||
|
-webkit-border-radius: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #B3B3B3;
|
||||||
|
box-shadow: 0px 0px 18px #B3B3B3;
|
||||||
|
margin: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window .terminalizer-titlebar {
|
||||||
|
-moz-border-top-left-radius: 6px;
|
||||||
|
-moz-border-top-right-radius: 6px;
|
||||||
|
-webkit-border-top-left-radius: 6px;
|
||||||
|
-webkit-border-top-right-radius: 6px;
|
||||||
|
background: #E8E8E8;
|
||||||
|
background: -moz-linear-gradient(top, #EBEBEB, #D6D6D6);
|
||||||
|
background: -ms-linear-gradient(top, #EBEBEB, #D6D6D6);
|
||||||
|
background: -o-linear-gradient(top, #EBEBEB, #D6D6D6);
|
||||||
|
background: -webkit-gradient(linear, left top, left bottom, color-stop(0.0, #EBEBEB, color-stop(1.0, #D6D6D6)));
|
||||||
|
background: -webkit-linear-gradient(top, #EBEBEB, #D6D6D6);
|
||||||
|
background: linear-gradient(top, #EBEBEB, #D6D6D6);
|
||||||
|
border-bottom: 1px solid #B1AEB1;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
border-top: 1px solid #F3F1F3;
|
||||||
|
color: #3B4247;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window .terminalizer-titlebar .buttons {
|
||||||
|
left: 8px;
|
||||||
|
line-height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
top: 3.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window .terminalizer-titlebar .close {
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
background: #FF5C5C;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #E33E41;
|
||||||
|
display: inline-block;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window .terminalizer-titlebar .minimize {
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
background: #FFBD4C;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #E09E3E;
|
||||||
|
display: inline-block;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window .terminalizer-titlebar .maximize {
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
background: #00CA56;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #14AE46;
|
||||||
|
display: inline-block;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-window .terminalizer-body {
|
||||||
|
background-color: #1D1D1D;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//// [ Floating ] ////*/
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating {
|
||||||
|
-moz-border-radius: 6px;
|
||||||
|
-webkit-border-radius: 6px;
|
||||||
|
background-color: #1D1D1D;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 0px 18px #B3B3B3;
|
||||||
|
margin: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating .terminalizer-titlebar {
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating .terminalizer-titlebar .buttons {
|
||||||
|
left: 13px;
|
||||||
|
line-height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating .terminalizer-titlebar .close {
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
background: #FF5C5C;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
height: 15px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating .terminalizer-titlebar .minimize {
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
background: #FFBD4C;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
height: 15px;
|
||||||
|
line-height: 10px;
|
||||||
|
margin-left: 4px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating .terminalizer-titlebar .maximize {
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
background: #00CA56;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
height: 15px;
|
||||||
|
line-height: 10px;
|
||||||
|
margin-left: 4px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-floating .terminalizer-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//// [ Solid ] ////*/
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-solid {
|
||||||
|
-moz-border-radius: 6px;
|
||||||
|
-webkit-border-radius: 6px;
|
||||||
|
background-color: #1D1D1D;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 0px 18px #B3B3B3;
|
||||||
|
margin: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-solid .terminalizer-titlebar {
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-solid .terminalizer-titlebar .title {
|
||||||
|
margin: 15px 15px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-solid .terminalizer-titlebar .title:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-solid .terminalizer-titlebar .buttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalizer-frame.terminalizer-solid .terminalizer-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
300
lib/terminalizer.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
/**
|
||||||
|
* Terminalizer Web Plugin
|
||||||
|
* https://terminalizer.com
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
|
||||||
|
$.fn.terminalizer = function(options) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target object
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The terminal instance
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
var term = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recording file
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
var data = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML template code
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
var template = '<div class="terminalizer">' +
|
||||||
|
'<div class="terminalizer-frame"><div class="terminalizer-titlebar">' +
|
||||||
|
'<div class="buttons"><div class="close"></div><div class="minimize">' +
|
||||||
|
'</div><div class="maximize"></div></div><div class="title"></div>' +
|
||||||
|
'</div><div class="terminalizer-body"></div></div></div>';
|
||||||
|
|
||||||
|
// Default options
|
||||||
|
options = $.extend({
|
||||||
|
recordingFile: null,
|
||||||
|
realTiming: false,
|
||||||
|
speedFactor: 1.0,
|
||||||
|
beforeMiddleware: null,
|
||||||
|
afterMiddleware: null
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
// Load the recording file
|
||||||
|
loadJSON(options.recordingFile).then(function(result) {
|
||||||
|
|
||||||
|
// Cache the data
|
||||||
|
data = result;
|
||||||
|
|
||||||
|
// Marge the plugin's options with recording file's configs
|
||||||
|
options = $.extend(data.config, options);
|
||||||
|
|
||||||
|
// Terminal
|
||||||
|
term = new Terminal({
|
||||||
|
cols: options.cols,
|
||||||
|
rows: options.rows,
|
||||||
|
cursorStyle: options.cursorStyle,
|
||||||
|
fontFamily: options.fontFamily,
|
||||||
|
fontSize: options.fontSize,
|
||||||
|
lineHeight: options.lineHeight,
|
||||||
|
letterSpacing: options.letterSpacing,
|
||||||
|
allowTransparency: true,
|
||||||
|
theme: options.theme
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert the html template
|
||||||
|
self.html($(template));
|
||||||
|
|
||||||
|
if (options.frameBox.type) {
|
||||||
|
self.find('.terminalizer-frame').addClass('terminalizer-' + options.frameBox.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.frameBox.type && options.frameBox.title) {
|
||||||
|
self.find('.terminalizer-frame .title').text(options.frameBox.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.find('.terminalizer-frame').css(options.frameBox.style);
|
||||||
|
|
||||||
|
// Open the terminal
|
||||||
|
term.open(self.find('.terminalizer-body')[0]);
|
||||||
|
term.focus();
|
||||||
|
|
||||||
|
// Add a watermark
|
||||||
|
if (options.watermark.imagePath) {
|
||||||
|
return self._addWatermark(options.watermark);
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then(function() {
|
||||||
|
|
||||||
|
// Play
|
||||||
|
play(data.records, self._playCallback, self._doneCallback, options);
|
||||||
|
|
||||||
|
}).catch(function(error) {
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a watermark and wait until it is fully loaded
|
||||||
|
*
|
||||||
|
* @param {Object} watermarkConfig {imagePath, style}
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
this._addWatermark = function(watermarkConfig) {
|
||||||
|
|
||||||
|
var watermarkImg = document.createElement('img');
|
||||||
|
|
||||||
|
$(watermarkImg).addClass('terminalizer-watermark');
|
||||||
|
$(watermarkImg).attr('src', watermarkConfig.imagePath);
|
||||||
|
$(watermarkImg).css(watermarkConfig.style);
|
||||||
|
|
||||||
|
this.find('.terminalizer-frame').prepend(watermarkImg);
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
|
||||||
|
$('.terminalizer-watermark').on('load', resolve);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed after all frames are played
|
||||||
|
*/
|
||||||
|
this._doneCallback = function() {
|
||||||
|
|
||||||
|
self.trigger('playingDone');
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the passed record's content
|
||||||
|
*
|
||||||
|
* @param {String} record
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
this._playCallback = function(record, callback) {
|
||||||
|
|
||||||
|
var tasks = [];
|
||||||
|
|
||||||
|
// The beforeMiddleware is set
|
||||||
|
if (options.beforeMiddleware) {
|
||||||
|
|
||||||
|
tasks.push(function(callback) {
|
||||||
|
options.beforeMiddleware.call(self, record, callback.bind(null, null, null));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
tasks.push(function(callback) {
|
||||||
|
|
||||||
|
term.write(record.content);
|
||||||
|
|
||||||
|
// Wrokaround since xterm doesn't provide a rendered event
|
||||||
|
var renderCheckTimer = setInterval(function() {
|
||||||
|
|
||||||
|
if (term.writeInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(renderCheckTimer);
|
||||||
|
callback();
|
||||||
|
|
||||||
|
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// The afterMiddleware is set
|
||||||
|
if (options.afterMiddleware) {
|
||||||
|
|
||||||
|
tasks.push(function(callback) {
|
||||||
|
options.afterMiddleware.call(self, record, callback.bind(null, null, null));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async.series(tasks, function(error, result) {
|
||||||
|
|
||||||
|
callback();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load, and parse JSON files
|
||||||
|
*
|
||||||
|
* @param {String} url
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
function loadJSON(url) {
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
|
||||||
|
$.getJSON(url).done(resolve).fail(function(jqxhr, textStatus, error) {
|
||||||
|
reject('Failed to load ' + url);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play recording records
|
||||||
|
*
|
||||||
|
* 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 prevnt limiting the max idle time
|
||||||
|
*
|
||||||
|
* - speedFactor (default: 1)
|
||||||
|
* - Multiply the frames delays by this factor
|
||||||
|
*
|
||||||
|
* @param {Array} records
|
||||||
|
* @param {Function} playCallback
|
||||||
|
* @param {Function|Null} doneCallback
|
||||||
|
* @param {Object} options (optional)
|
||||||
|
*/
|
||||||
|
function play(records, playCallback, doneCallback, options) {
|
||||||
|
|
||||||
|
var tasks = [];
|
||||||
|
|
||||||
|
// 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, index) {
|
||||||
|
|
||||||
|
// Create a task to handle each frame
|
||||||
|
tasks.push(function(callback) {
|
||||||
|
|
||||||
|
var delay = record.delay;
|
||||||
|
|
||||||
|
// Adjust the delay according to the options
|
||||||
|
if (options.frameDelay != 'auto') {
|
||||||
|
delay = options.frameDelay;
|
||||||
|
} else if (options.maxIdleTime != 'auto' && delay > options.maxIdleTime) {
|
||||||
|
delay = options.maxIdleTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply speedFactor
|
||||||
|
delay = delay * options.speedFactor;
|
||||||
|
|
||||||
|
// Add an index to the record object
|
||||||
|
record.index = index;
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
playCallback(record, callback);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
async.series(tasks, function(error, results) {
|
||||||
|
|
||||||
|
if (doneCallback) {
|
||||||
|
doneCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}(jQuery));
|
||||||
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "terminalizer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Record your terminal and generate animated gif images",
|
||||||
|
"main": "bin/app.js",
|
||||||
|
"author": "Mohammad Fares <faressoft.com@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/faressoft/terminalizer.git"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"terminalizer": "bin/app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^2.6.1",
|
||||||
|
"async-promises": "^0.2.1",
|
||||||
|
"death": "^1.1.0",
|
||||||
|
"deepmerge": "^2.1.0",
|
||||||
|
"electron": "^2.0.5",
|
||||||
|
"fs-extra": "^5.0.0",
|
||||||
|
"gif-encoder": "^0.6.1",
|
||||||
|
"is_js": "^0.9.0",
|
||||||
|
"jquery": "^3.3.1",
|
||||||
|
"js-yaml": "^3.11.0",
|
||||||
|
"node-pty-prebuilt": "^0.7.3",
|
||||||
|
"performance-now": "^2.1.0",
|
||||||
|
"pngjs": "^3.3.2",
|
||||||
|
"progress": "^2.0.0",
|
||||||
|
"string-argv": "0.0.2",
|
||||||
|
"xterm": "^3.5.1",
|
||||||
|
"yargs": "^12.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
124
render/index.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Terminalizer</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="../node_modules/xterm/dist/xterm.css">
|
||||||
|
<link rel="stylesheet" href="../lib/terminalizer.css">
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="terminal"></div>
|
||||||
|
|
||||||
|
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||||
|
|
||||||
|
<script src="../node_modules/xterm/dist/xterm.js"></script>
|
||||||
|
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script src="../node_modules/async/dist/async.min.js"></script>
|
||||||
|
<script src="../lib/terminalizer.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
var fs = require('fs'),
|
||||||
|
path = require('path'),
|
||||||
|
remote = require('electron').remote,
|
||||||
|
ipcRenderer = require('electron').ipcRenderer;
|
||||||
|
var currentWindow = remote.getCurrentWindow(),
|
||||||
|
capturePage = currentWindow.webContents.capturePage,
|
||||||
|
step = remote.getGlobal('step');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for the step option
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
var stepsCounter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the terminalizer plugin
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
var options = {
|
||||||
|
recordingFile: 'data.json',
|
||||||
|
frameDelay: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A middleware that called after rendering frames
|
||||||
|
*
|
||||||
|
* @param {Object} record {delay, content, index}
|
||||||
|
* @param {Function} next
|
||||||
|
*/
|
||||||
|
options.afterMiddleware = function(record, next) {
|
||||||
|
|
||||||
|
var width = this.width();
|
||||||
|
var height = this.height();
|
||||||
|
var captureRect = {x: 0, y: 0, width: width, height: height};
|
||||||
|
|
||||||
|
if (stepsCounter != 0) {
|
||||||
|
stepsCounter = (stepsCounter + 1) % step;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
stepsCounter = (stepsCounter + 1) % step;
|
||||||
|
|
||||||
|
// A workaround by a delay to make sure the record is rendered
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
capturePage(captureRect, function(img) {
|
||||||
|
|
||||||
|
var outputPath = path.join(__dirname, '/frames/' + record.index + '.png');
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, img.toPNG());
|
||||||
|
ipcRenderer.send('captured', record.index);
|
||||||
|
next();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the terminalizer plugin
|
||||||
|
$('#terminal').terminalizer(options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function for the event:
|
||||||
|
* playingDone
|
||||||
|
*/
|
||||||
|
$('#terminal').on('playingDone', function() {
|
||||||
|
|
||||||
|
currentWindow.close();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catch all unhandled errors
|
||||||
|
*
|
||||||
|
* @param {String} errorMsg
|
||||||
|
*/
|
||||||
|
window.onerror = function(errorMsg) {
|
||||||
|
|
||||||
|
ipcRenderer.send('error', errorMsg);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>if (window.module) module = window.module;</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
63
render/index.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Render the frames into PNG images
|
||||||
|
* An electron app, takes one command line argument `step`
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
var path = require('path'),
|
||||||
|
app = require('electron').app,
|
||||||
|
BrowserWindow = require('electron').BrowserWindow,
|
||||||
|
ipcMain = require('electron').ipcMain;
|
||||||
|
|
||||||
|
// Set as global to be read by the web page
|
||||||
|
global.step = process.argv[2] || 1;
|
||||||
|
|
||||||
|
// Hide the Dock for macOS
|
||||||
|
app.dock.hide();
|
||||||
|
|
||||||
|
// Set the display scale factor to 1
|
||||||
|
app.commandLine.appendSwitch('force-device-scale-factor', 1);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
var win = new BrowserWindow({show: false});
|
||||||
|
|
||||||
|
// Load index.html
|
||||||
|
win.loadURL('file://' + __dirname + '/index.html');
|
||||||
|
|
||||||
|
// Maximize the window
|
||||||
|
win.maximize();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function for the event:
|
||||||
|
* When a frame is captured
|
||||||
|
*
|
||||||
|
* @param {Number} recordIndex
|
||||||
|
*/
|
||||||
|
ipcMain.on('captured', function(recordIndex) {
|
||||||
|
|
||||||
|
console.log(recordIndex);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function for the event:
|
||||||
|
* When something unexpected happened
|
||||||
|
*
|
||||||
|
* @param {String} errorMsg
|
||||||
|
*/
|
||||||
|
ipcMain.on('error', function(errorMsg) {
|
||||||
|
|
||||||
|
console.error(errorMsg);
|
||||||
|
|
||||||
|
});
|
||||||
171
utility.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* Provide utility functions
|
||||||
|
*
|
||||||
|
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @param {String} filePath an absolute or a relative path
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
function loadYAML(filePath) {
|
||||||
|
|
||||||
|
var file = null;
|
||||||
|
|
||||||
|
// Rsolve the path into an absolute path
|
||||||
|
filePath = di.path.resolve(filePath);
|
||||||
|
|
||||||
|
// The file doesn't exist
|
||||||
|
if (!di.fs.existsSync(filePath)) {
|
||||||
|
|
||||||
|
// A file with .yml suffix also doesn't exist
|
||||||
|
if (!di.fs.existsSync(filePath + '.yml')) {
|
||||||
|
throw new Error('The provided file doesn\'t exit');
|
||||||
|
} else {
|
||||||
|
filePath = filePath + '.yml';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file
|
||||||
|
try {
|
||||||
|
file = di.fs.readFileSync(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
*
|
||||||
|
* @param {String} filePath an absolute or a relative path
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
function loadJSON(filePath) {
|
||||||
|
|
||||||
|
var file = null;
|
||||||
|
|
||||||
|
// Rsolve the path into an absolute path
|
||||||
|
filePath = di.path.resolve(filePath);
|
||||||
|
|
||||||
|
// The file doesn't exist
|
||||||
|
if (!di.fs.existsSync(filePath)) {
|
||||||
|
|
||||||
|
// A file with .json suffix also doesn't exist
|
||||||
|
if (!di.fs.existsSync(filePath + '.json')) {
|
||||||
|
throw new Error('The provided file doesn\'t exit');
|
||||||
|
} else {
|
||||||
|
filePath = 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 a path and add an extension to the file name
|
||||||
|
*
|
||||||
|
* - Add the extension if not already added
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*
|
||||||
|
* @return {Object} {json, raw}
|
||||||
|
*/
|
||||||
|
function getDefaultConfig() {
|
||||||
|
|
||||||
|
var filePath = di.path.join(__dirname, 'config.yml');
|
||||||
|
|
||||||
|
return loadYAML(filePath);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a value for a specific key in YAML
|
||||||
|
*
|
||||||
|
* - Works only with the first level keys
|
||||||
|
* - Workds 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// Module //////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadYAML: loadYAML,
|
||||||
|
loadJSON: loadJSON,
|
||||||
|
resolveFilePath: resolveFilePath,
|
||||||
|
getDefaultConfig: getDefaultConfig,
|
||||||
|
changeYAMLValue: changeYAMLValue
|
||||||
|
};
|
||||||