Browse Source

Switch to node-pty-prebuilt-multiarch and upgrade Electron

master
Mohammad Fares 3 months ago
parent
commit
be7b1533d7
  1. 5
      .prettierrc
  2. 2
      app.js
  3. 340
      commands/render.js
  4. 16
      package.json
  5. 79
      render/index.js
  6. 19
      render/preload.js
  7. 107
      render/src/js/app.js
  8. 27
      webpack.config.js
  9. 2918
      yarn.lock

5
.prettierrc

@ -0,0 +1,5 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 120
}

2
app.js

@ -41,7 +41,7 @@ di.require('progress', 'ProgressBar');
di.require('gif-encoder', 'GIFEncoder');
di.require('inquirer');
di.set('pty', require('@faressoft/node-pty-prebuilt'));
di.set('pty', require('node-pty-prebuilt-multiarch'));
di.set('PNG', require('pngjs').PNG);
di.set('spawn', require('child_process').spawn);
di.set('utility', require('./utility.js'));

340
commands/render.js

@ -1,7 +1,7 @@
/**
* Render
* Render a recording file as an animated gif image
*
*
* @author Mohammad Fares <faressoft.com@gmail.com>
*/
@ -13,37 +13,40 @@
* @return {ProgressBar}
*/
function getProgressBar(operation, framesCount) {
return new di.ProgressBar(operation + ' ' + di.chalk.magenta('frame :current/:total') + ' :percent [:bar] :etas', {
width: 30,
total: 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) {
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) {
di.fs.writeFile(
di.path.join(ROOT_PATH, "render/data.json"),
JSON.stringify(recordingFile.json),
"utf8",
function (error) {
if (error) {
return reject(error);
}
if (error) {
return reject(error);
resolve();
}
resolve();
});
);
});
}
/**
@ -53,80 +56,72 @@ function writeRecordingData(recordingFile) {
* @return {Promise} resolve with the parsed PNG image
*/
function loadPNG(path) {
return new Promise(function(resolve, reject) {
di.fs.readFile(path, function(error, imageData) {
return new Promise(function (resolve, reject) {
di.fs.readFile(path, function (error, imageData) {
if (error) {
return reject(error);
}
new di.PNG().parse(imageData, function(error, data) {
new di.PNG().parse(imageData, function (error, data) {
if (error) {
return reject(error);
}
resolve(data);
});
});
});
}
/**
* Get the dimensions of the first rendered frame
*
*
* @return {Promise}
*/
function getFrameDimensions() {
// The path of the first rendered frame
var framePath = di.path.join(ROOT_PATH, 'render/frames/0.png');
var framePath = di.path.join(ROOT_PATH, "render/frames/0.png");
// Read and parse a PNG image file
return loadPNG(framePath).then(function(png) {
return({
return loadPNG(framePath).then(function (png) {
return {
width: png.width,
height: png.height
});
height: png.height,
};
});
}
/**
* Render the frames into PNG images
*
*
* @param {Array} records [{delay, content}, ...]
* @param {Object} options {step}
* @return {Promise}
*/
function renderFrames(records, options) {
return new Promise(function(resolve, reject) {
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));
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});
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.stderr.on("data", function (error) {
render.kill();
reject(new Error(error));
});
render.stdout.on('data', function(data) {
render.stdout.on("data", function (data) {
// Is not a recordIndex (to skip Electron's logs or new lines)
if (di.is.not.number(parseInt(data.toString()))) {
return;
@ -138,25 +133,20 @@ function renderFrames(records, options) {
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) {
return new Promise(function (resolve, reject) {
// The number of frames
var framesCount = records.length;
@ -164,11 +154,14 @@ function mergeFrames(records, options, frameDimensions) {
var stepsCounter = 0;
// Create a progress bar
var progressBar = getProgressBar('Merging', Math.ceil(framesCount / options.step));
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
highWaterMark: 5 * 1024 * 1024,
});
// Pipe
@ -184,101 +177,91 @@ function mergeFrames(records, options, frameDimensions) {
gif.writeHeader();
// Foreach frame
di.async.eachOfSeries(records, function(frame, index, callback) {
di.async.eachOfSeries(
records,
function (frame, index, callback) {
if (stepsCounter != 0) {
stepsCounter = (stepsCounter + 1) % options.step;
return 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
loadPNG(framePath).then(function(png) {
progressBar.tick();
// Set the duration (the delay of the next frame)
// The % is used to take the delay of the first frame
// as the duration of the last frame
gif.setDelay(records[(index + 1) % framesCount].delay);
// Add frames
gif.addFrame(png.data);
// Next
callback();
}).catch(function(error) {
callback(error);
});
}, function(error) {
// The path of the rendered frame
var framePath = di.path.join(
ROOT_PATH,
"render/frames",
index + ".png"
);
// Read and parse the rendered frame
loadPNG(framePath)
.then(function (png) {
progressBar.tick();
// Set the duration (the delay of the next frame)
// The % is used to take the delay of the first frame
// as the duration of the last frame
gif.setDelay(records[(index + 1) % framesCount].delay);
// Add frames
gif.addFrame(png.data);
// Next
callback();
})
.catch(function (error) {
callback(error);
});
},
function (error) {
if (error) {
return reject(error);
}
if (error) {
return reject(error);
// Write the footer
gif.finish();
resolve();
}
// Write the footer
gif.finish();
resolve();
});
);
});
}
/**
* Delete the temporary rendered PNG images
*
* 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) {
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("\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;
@ -287,17 +270,20 @@ function command(argv) {
var framesCount = records.length;
// The path of the output file
var outputFile = di.utility.resolveFilePath('render' + (new Date()).getTime(), 'gif');
var outputFile = di.utility.resolveFilePath(
"render" + new Date().getTime(),
"gif"
);
// For adjusting (calculating) the frames delays
var adjustFramesDelaysOptions = {
frameDelay: config.frameDelay,
maxIdleTime: config.maxIdleTime
maxIdleTime: config.maxIdleTime,
};
// For rendering the frames into PNG images
var renderingOptions = {
step: argv.step
step: argv.step,
};
// For merging the rendered frames into an animated GIF image
@ -305,7 +291,7 @@ function command(argv) {
quality: config.quality,
repeat: config.repeat,
step: argv.step,
outputFile: outputFile
outputFile: outputFile,
};
// Overwrite the quality of the rendered image
@ -320,35 +306,37 @@ function command(argv) {
}
// Tasks
di.asyncPromises.waterfall([
// Remove all previously rendered frames
cleanup,
// Write the recording data into render/data.json
di._.partial(writeRecordingData, argv.recordingFile),
// Render the frames into PNG images
di._.partial(renderFrames, records, renderingOptions),
// Adjust frames delays
di._.partial(di.commands.play.adjustFramesDelays, records, adjustFramesDelaysOptions),
// Get the dimensions of the first rendered frame
di._.partial(getFrameDimensions),
// Merge the rendered frames into an animated GIF image
di._.partial(mergeFrames, records, mergingOptions),
// Delete the temporary rendered PNG images
cleanup
]).then(function() {
done(outputFile);
}).catch(di.errorHandler);
di.asyncPromises
.waterfall([
// Remove all previously rendered frames
cleanup,
// Write the recording data into render/data.json
di._.partial(writeRecordingData, argv.recordingFile),
// Render the frames into PNG images
di._.partial(renderFrames, records, renderingOptions),
// Adjust frames delays
di._.partial(
di.commands.play.adjustFramesDelays,
records,
adjustFramesDelaysOptions
),
// Get the dimensions of the first rendered frame
di._.partial(getFrameDimensions),
// Merge the rendered frames into an animated GIF image
di._.partial(mergeFrames, records, mergingOptions),
// Delete the temporary rendered PNG images
cleanup,
])
.then(function () {
done(outputFile);
})
.catch(di.errorHandler);
}
////////////////////////////////////////////////////
@ -359,13 +347,13 @@ function command(argv) {
* Command's usage
* @type {String}
*/
module.exports.command = 'render <recordingFile>';
module.exports.command = "render <recordingFile>";
/**
* Command's description
* @type {String}
*/
module.exports.describe = 'Render a recording file as an animated gif image';
module.exports.describe = "Render a recording file as an animated gif image";
/**
* Command's handler function
@ -375,42 +363,40 @@ module.exports.handler = command;
/**
* Builder
*
*
* @param {Object} yargs
*/
module.exports.builder = function(yargs) {
module.exports.builder = function (yargs) {
// Define the recordingFile argument
yargs.positional('recordingFile', {
describe: 'The recording file',
type: 'string',
coerce: di.utility.loadYAML
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',
yargs.option("o", {
alias: "output",
type: "string",
describe: "A name for the output file",
requiresArg: true,
coerce: di._.partial(di.utility.resolveFilePath, di._, 'gif')
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
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)',
yargs.option("s", {
alias: "step",
type: "number",
describe: "To reduce the number of rendered frames (step > 1)",
requiresArg: true,
default: 1
default: 1,
});
};

16
package.json

@ -14,8 +14,8 @@
"terminalizer": "bin/app.js"
},
"scripts": {
"dev": "NODE_ENV=development webpack --colors --watch",
"build": "NODE_ENV=production webpack --optimize-minimize --progress --colors",
"dev": "NODE_ENV=development webpack --watch",
"build": "NODE_ENV=production webpack --progress",
"prepublish": "npm run build"
},
"keywords": [
@ -41,13 +41,12 @@
"pty"
],
"dependencies": {
"@faressoft/node-pty-prebuilt": "^0.9.0",
"async": "^2.6.3",
"async-promises": "^0.2.2",
"chalk": "^2.4.2",
"death": "^1.1.0",
"deepmerge": "^2.2.1",
"electron": "^15.5.5",
"electron": "17",
"flowa": "^4.0.2",
"fs-extra": "^5.0.0",
"gif-encoder": "^0.6.1",
@ -55,6 +54,7 @@
"is_js": "^0.9.0",
"js-yaml": "^3.13.1",
"lodash": "^4.17.15",
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
"performance-now": "^2.1.0",
"pngjs": "^3.4.0",
"progress": "^2.0.3",
@ -66,13 +66,13 @@
},
"devDependencies": {
"ajv": "^6.12.0",
"clean-webpack-plugin": "^0.1.19",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^1.0.0",
"jquery": "^3.4.1",
"mini-css-extract-plugin": "^0.4.5",
"mini-css-extract-plugin": "^2.6.1",
"terminalizer-player": "^0.4.1",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"xterm": "^3.14.5"
}
}

79
render/index.js

@ -1,28 +1,31 @@
/**
* 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,
os = require('os');
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
const { BrowserWindow } = require('electron');
const ipcMain = require('electron').ipcMain;
const os = require('os');
/**
* The step option
* To reduce the number of rendered frames (step > 1)
* @type {Number}
*/
global.step = process.argv[2] || 1;
let mainWindow = null;
/**
* The temporary rendering directory's path
* @type {String}
*/
global.renderDir = path.join(__dirname, 'frames');
var renderDir = path.join(__dirname, 'frames');
/**
* The step option
* To reduce the number of rendered frames (step > 1)
* @type {Number}
*/
var step = process.argv[2] || 1;
// Hide the Dock for macOS
if (os.platform() == 'darwin') {
@ -36,33 +39,53 @@ app.on('ready', createWindow);
* Create a hidden browser window and load the rendering page
*/
function createWindow() {
// Create a browser window
var win = new BrowserWindow({
mainWindow = new BrowserWindow({
show: false,
width: 8000,
height: 8000,
webPreferences: {
nodeIntegration: true
}
preload: path.join(__dirname, 'preload.js'),
},
});
// Load index.html
win.loadURL('file://' + __dirname + '/index.html');
// Load index.html
mainWindow.loadURL('file://' + __dirname + '/index.html');
}
/**
* A callback function for the event:
* When a frame is captured
*
* getOptions to request the options that need
* to be passed to the renderer
*
* @param {Object} event
*/
ipcMain.handle('getOptions', function () {
return { step };
});
/**
* A callback function for the event:
* capturePage
*
* @param {Object} event
* @param {Number} recordIndex
*/
ipcMain.on('captured', function(event, recordIndex) {
ipcMain.handle('capturePage', async function (event, captureRect, frameIndex) {
const img = await mainWindow.webContents.capturePage(captureRect);
const outputPath = path.join(renderDir, frameIndex + '.png');
fs.writeFileSync(outputPath, img.toPNG());
console.log(frameIndex);
});
console.log(recordIndex);
/**
* A callback function for the event:
* Close
*
* @param {Object} event
* @param {String} error
*/
ipcMain.on('close', function (event, error) {
mainWindow.close();
});
/**
@ -72,8 +95,6 @@ ipcMain.on('captured', function(event, recordIndex) {
* @param {Object} event
* @param {String} error
*/
ipcMain.on('error', function(event, error) {
ipcMain.on('error', function (event, error) {
process.stderr.write(error);
});

19
render/preload.js

@ -0,0 +1,19 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('app', {
close() {
return ipcRenderer.send('close');
},
getOptions() {
return ipcRenderer.invoke('getOptions');
},
capturePage(captureRect, frameIndex) {
console.log('prelaod > capturePage');
return ipcRenderer.invoke('capturePage', captureRect, frameIndex);
},
});
// Catch all unhandled errors
window.onerror = function (error) {
ipcRenderer.send('error', error);
};

107
render/src/js/app.js

@ -1,19 +1,11 @@
/**
* Terminalizer
*
*
* @author Mohammad Fares <faressoft.com@gmail.com>
*/
var fs = require('fs'),
path = require('path'),
async = require('async'),
remote = require('electron').remote,
ipcRenderer = require('electron').ipcRenderer,
terminalizerPlayer = require('terminalizer-player');
var currentWindow = remote.getCurrentWindow(),
capturePage = currentWindow.webContents.capturePage,
step = remote.getGlobal('step'),
renderDir = remote.getGlobal('renderDir');
import async from 'async';
import 'terminalizer-player';
// Styles
import '../css/app.css';
@ -26,119 +18,98 @@ import 'xterm/dist/xterm.css';
*/
var stepsCounter = 0;
/**
* Rendering options
*/
var options = {};
/**
* A callback function for the event:
* When the document is loaded
*/
$(document).ready(function() {
$(document).ready(async () => {
options = await app.getOptions();
// Initialize the terminalizer plugin
$('#terminal').terminalizer({
recordingFile: 'data.json',
autoplay: true,
controls: false
controls: false,
});
/**
* A callback function for the event:
* When the terminal playing is started
*/
$('#terminal').one('playingStarted', function() {
$('#terminal').one('playingStarted', function () {
var terminalizer = $('#terminal').data('terminalizer');
// Pause the playing
terminalizer.pause();
});
/**
* A callback function for the event:
* When the terminal playing is paused
*/
$('#terminal').one('playingPaused', function() {
$('#terminal').one('playingPaused', function () {
var terminalizer = $('#terminal').data('terminalizer');
// Reset the terminal
terminalizer._terminal.reset();
// When the terminal's reset is done
// When the terminal's reset is done
$('#terminal').one('rendered', render);
});
});
/**
* Render each frame and capture it
*/
function render() {
var terminalizer = $('#terminal').data('terminalizer');
var framesCount = terminalizer.getFramesCount();
// Foreach frame
async.timesSeries(framesCount, function(frameIndex, next) {
terminalizer._renderFrame(frameIndex, true, function() {
capture(frameIndex, next);
});
}, function(error) {
if (error) {
throw new Error(error);
async.timesSeries(
framesCount,
function (frameIndex, next) {
terminalizer._renderFrame(frameIndex, true, function () {
capture(frameIndex, next);
});
},
function (error) {
if (error) {
throw new Error(error);
}
app.close();
}
currentWindow.close();
});
);
}
/**
* Capture the current frame
*
*
* @param {Number} frameIndex
* @param {Function} callback
*/
function capture(frameIndex, callback) {
var width = $('#terminal').width();
var height = $('#terminal').height();
var captureRect = {x: 0, y: 0, width: width, height: height};
var captureRect = { x: 0, y: 0, width: width, height: height };
if (stepsCounter != 0) {
stepsCounter = (stepsCounter + 1) % step;
stepsCounter = (stepsCounter + 1) % options.step;
return callback();
}
stepsCounter = (stepsCounter + 1) % step;
capturePage(captureRect).then((img) => {
var outputPath = path.join(renderDir, frameIndex + '.png');
fs.writeFileSync(outputPath, img.toPNG());
ipcRenderer.send('captured', frameIndex);
callback();
}).catch((err) => {
throw new err;
});
stepsCounter = (stepsCounter + 1) % options.step;
app
.capturePage(captureRect, frameIndex)
.then(callback)
.catch((err) => {
throw err;
});
}
/**
* Catch all unhandled errors
*
* @param {String} error
*/
window.onerror = function(error) {
ipcRenderer.send('error', error);
};

27
webpack.config.js

@ -3,7 +3,7 @@ const path = require('path');
// Extract CSS into separate files
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// Global variables
const globals = {
@ -11,36 +11,35 @@ const globals = {
jQuery: 'jquery',
Terminal: ['xterm', 'Terminal'],
'window.jQuery': 'jquery',
'window.$': 'jquery'
'window.$': 'jquery',
};
module.exports = {
mode: 'production',
target: 'electron-renderer',
entry: {
app: './render/src/js/app.js'
app: './render/src/js/app.js',
},
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, 'render/dist'),
publicPath: '/dist/'
publicPath: '/dist/',
},
plugins: [
new CleanWebpackPlugin(['./render/dist'], {verbose: false}),
new CleanWebpackPlugin({
cleanBeforeEveryBuildPatterns: [path.join(__dirname, 'render/dist')],
}),
new webpack.ProvidePlugin(globals),
new MiniCssExtractPlugin({filename: 'css/[name].css'}),
new webpack.NoEmitOnErrorsPlugin()
new MiniCssExtractPlugin({ filename: 'css/[name].css' }),
new webpack.NoEmitOnErrorsPlugin(),
],
module: {
rules: [
// CSS
{
test: /\.css$/,
use: [
{loader: MiniCssExtractPlugin.loader},
{loader: 'css-loader'}
],
}
]
}
use: [{ loader: MiniCssExtractPlugin.loader }, { loader: 'css-loader' }],
},
],
},
};

2918
yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save