commit
d1153e58ba
28 changed files with 2697 additions and 0 deletions
-
19.gitignore
-
73.jscsrc
-
369README.md
-
96app.js
-
9bin/app.js
-
53commands/config.js
-
65commands/generate.js
-
231commands/play.js
-
273commands/record.js
-
381commands/render.js
-
65commands/share.js
-
106config.yml
-
43di.js
-
BINimg/demo.gif
-
BINimg/frames/floating.gif
-
BINimg/frames/null.gif
-
BINimg/frames/solid.gif
-
BINimg/frames/solid_without_title.gif
-
BINimg/frames/solid_without_title_without_shadows.gif
-
BINimg/frames/window.gif
-
BINimg/install.gif
-
BINimg/watermark.gif
-
222lib/terminalizer.css
-
300lib/terminalizer.js
-
34package.json
-
124render/index.html
-
63render/index.js
-
171utility.js
@ -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 |
@ -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 |
|||
} |
|||
} |
@ -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. |
@ -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); |
|||
|
|||
} |
@ -0,0 +1,9 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
/** |
|||
* Terminalizer |
|||
* |
|||
* @author Mohammad Fares <faressoft.com@gmail.com> |
|||
*/ |
|||
|
|||
require('../app.js'); |
@ -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; |
@ -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 |
|||
}); |
|||
|
|||
}; |
@ -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; |
@ -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'); |
|||
|
|||
}; |
@ -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 |
|||
}); |
|||
|
|||
}; |
@ -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 |
|||
}); |
|||
|
|||
}; |
@ -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 |
|||
|
|||