Sync up with master
This commit is contained in:
commit
193c203a05
|
@ -59,8 +59,8 @@ exports.resetScreen = resetScreen;
|
||||||
exports.normal = normal;
|
exports.normal = normal;
|
||||||
exports.goHome = goHome;
|
exports.goHome = goHome;
|
||||||
exports.disableVT100LineWrapping = disableVT100LineWrapping;
|
exports.disableVT100LineWrapping = disableVT100LineWrapping;
|
||||||
exports.setSyncTERMFont = setSyncTERMFont;
|
exports.setSyncTermFont = setSyncTermFont;
|
||||||
exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
|
exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias;
|
||||||
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
||||||
exports.setCursorStyle = setCursorStyle;
|
exports.setCursorStyle = setCursorStyle;
|
||||||
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
||||||
|
@ -320,7 +320,7 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function setSyncTERMFont(name, fontPage) {
|
function setSyncTermFont(name, fontPage) {
|
||||||
const p1 = miscUtil.valueWithDefault(fontPage, 0);
|
const p1 = miscUtil.valueWithDefault(fontPage, 0);
|
||||||
|
|
||||||
assert(p1 >= 0 && p1 <= 3);
|
assert(p1 >= 0 && p1 <= 3);
|
||||||
|
@ -333,13 +333,13 @@ function setSyncTERMFont(name, fontPage) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSyncTERMFontFromAlias(alias) {
|
function getSyncTermFontFromAlias(alias) {
|
||||||
return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')];
|
return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')];
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSyncTermFontWithAlias(nameOrAlias) {
|
function setSyncTermFontWithAlias(nameOrAlias) {
|
||||||
nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias;
|
nameOrAlias = getSyncTermFontFromAlias(nameOrAlias) || nameOrAlias;
|
||||||
return setSyncTERMFont(nameOrAlias);
|
return setSyncTermFont(nameOrAlias);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEC_CURSOR_STYLE = {
|
const DEC_CURSOR_STYLE = {
|
||||||
|
|
133
core/art.js
133
core/art.js
|
@ -15,7 +15,6 @@ const paths = require('path');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const xxhash = require('xxhash');
|
|
||||||
|
|
||||||
exports.getArt = getArt;
|
exports.getArt = getArt;
|
||||||
exports.getArtFromPath = getArtFromPath;
|
exports.getArtFromPath = getArtFromPath;
|
||||||
|
@ -250,8 +249,7 @@ function display(client, art, options, cb) {
|
||||||
return cb(Errors.Invalid('No art supplied!'));
|
return cb(Errors.Invalid('No art supplied!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
options.mciReplaceChar = options.mciReplaceChar || ' ';
|
options.mciReplaceChar = options.mciReplaceChar || ' ';
|
||||||
options.disableMciCache = options.disableMciCache || false;
|
|
||||||
|
|
||||||
// :TODO: this is going to be broken into two approaches controlled via options:
|
// :TODO: this is going to be broken into two approaches controlled via options:
|
||||||
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
|
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
|
||||||
|
@ -272,102 +270,67 @@ function display(client, art, options, cb) {
|
||||||
startRow : options.startRow,
|
startRow : options.startRow,
|
||||||
});
|
});
|
||||||
|
|
||||||
let parseComplete = false;
|
const mciMap = {};
|
||||||
let mciMap;
|
let generatedId = 100;
|
||||||
const mciCprQueue = [];
|
|
||||||
let artHash;
|
|
||||||
let mciMapFromCache;
|
|
||||||
|
|
||||||
function completed() {
|
ansiParser.on('mci', mciInfo => {
|
||||||
|
// :TODO: ensure generatedId's do not conflict with any existing |id|
|
||||||
|
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
|
||||||
|
const mapKey = `${mciInfo.mci}${id}`;
|
||||||
|
const mapEntry = mciMap[mapKey];
|
||||||
|
|
||||||
if(!options.disableMciCache && !mciMapFromCache) {
|
if(mapEntry) {
|
||||||
// cache our MCI findings...
|
mapEntry.focusSGR = mciInfo.SGR;
|
||||||
client.mciCache[artHash] = mciMap;
|
mapEntry.focusArgs = mciInfo.args;
|
||||||
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache');
|
} else {
|
||||||
|
mciMap[mapKey] = {
|
||||||
|
position : mciInfo.position,
|
||||||
|
args : mciInfo.args,
|
||||||
|
SGR : mciInfo.SGR,
|
||||||
|
code : mciInfo.mci,
|
||||||
|
id : id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!mciInfo.id) {
|
||||||
|
++generatedId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ansiParser.removeAllListeners(); // :TODO: Necessary???
|
});
|
||||||
|
|
||||||
|
ansiParser.on('literal', literal => client.term.write(literal, false) );
|
||||||
|
ansiParser.on('control', control => client.term.rawWrite(control) );
|
||||||
|
|
||||||
|
ansiParser.on('complete', () => {
|
||||||
|
ansiParser.removeAllListeners();
|
||||||
|
|
||||||
const extraInfo = {
|
const extraInfo = {
|
||||||
height : ansiParser.row - 1,
|
height : ansiParser.row - 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
return cb(null, mciMap, extraInfo);
|
return cb(null, mciMap, extraInfo);
|
||||||
}
|
|
||||||
|
|
||||||
if(!options.disableMciCache) {
|
|
||||||
artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE);
|
|
||||||
|
|
||||||
// see if we have a mciMap cached for this art
|
|
||||||
if(client.mciCache) {
|
|
||||||
mciMap = client.mciCache[artHash];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mciMap) {
|
|
||||||
mciMapFromCache = true;
|
|
||||||
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache');
|
|
||||||
} else {
|
|
||||||
// no cached MCI info
|
|
||||||
mciMap = {};
|
|
||||||
|
|
||||||
let generatedId = 100;
|
|
||||||
|
|
||||||
ansiParser.on('mci', mciInfo => {
|
|
||||||
// :TODO: ensure generatedId's do not conflict with any existing |id|
|
|
||||||
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
|
|
||||||
const mapKey = `${mciInfo.mci}${id}`;
|
|
||||||
const mapEntry = mciMap[mapKey];
|
|
||||||
|
|
||||||
if(mapEntry) {
|
|
||||||
mapEntry.focusSGR = mciInfo.SGR;
|
|
||||||
mapEntry.focusArgs = mciInfo.args;
|
|
||||||
} else {
|
|
||||||
mciMap[mapKey] = {
|
|
||||||
position : mciInfo.position,
|
|
||||||
args : mciInfo.args,
|
|
||||||
SGR : mciInfo.SGR,
|
|
||||||
code : mciInfo.mci,
|
|
||||||
id : id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!mciInfo.id) {
|
|
||||||
++generatedId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ansiParser.on('literal', literal => client.term.write(literal, false) );
|
|
||||||
ansiParser.on('control', control => client.term.rawWrite(control) );
|
|
||||||
|
|
||||||
ansiParser.on('complete', () => {
|
|
||||||
parseComplete = true;
|
|
||||||
|
|
||||||
if(0 === mciCprQueue.length) {
|
|
||||||
return completed();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let initSeq = '';
|
let initSeq = '';
|
||||||
if(options.font) {
|
if (client.term.syncTermFontsEnabled) {
|
||||||
initSeq = ansi.setSyncTermFontWithAlias(options.font);
|
if(options.font) {
|
||||||
} else if(options.sauce) {
|
initSeq = ansi.setSyncTermFontWithAlias(options.font);
|
||||||
let fontName = getFontNameFromSAUCE(options.sauce);
|
} else if(options.sauce) {
|
||||||
if(fontName) {
|
let fontName = getFontNameFromSAUCE(options.sauce);
|
||||||
fontName = ansi.getSyncTERMFontFromAlias(fontName);
|
if(fontName) {
|
||||||
}
|
fontName = ansi.getSyncTermFontFromAlias(fontName);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Set SyncTERM font if we're switching only. Most terminals
|
// Set SyncTERM font if we're switching only. Most terminals
|
||||||
// that support this ESC sequence can only show *one* font
|
// that support this ESC sequence can only show *one* font
|
||||||
// at a time. This applies to detection only (e.g. SAUCE).
|
// at a time. This applies to detection only (e.g. SAUCE).
|
||||||
// If explicit, we'll set it no matter what (above)
|
// If explicit, we'll set it no matter what (above)
|
||||||
//
|
//
|
||||||
if(fontName && client.term.currentSyncFont != fontName) {
|
if(fontName && client.term.currentSyncFont != fontName) {
|
||||||
client.term.currentSyncFont = fontName;
|
client.term.currentSyncFont = fontName;
|
||||||
initSeq = ansi.setSyncTERMFont(fontName);
|
initSeq = ansi.setSyncTermFont(fontName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,13 +87,8 @@ function Client(/*input, output*/) {
|
||||||
this.lastActivityTime = Date.now();
|
this.lastActivityTime = Date.now();
|
||||||
this.menuStack = new MenuStack(this);
|
this.menuStack = new MenuStack(this);
|
||||||
this.acs = new ACS( { client : this, user : this.user } );
|
this.acs = new ACS( { client : this, user : this.user } );
|
||||||
this.mciCache = {};
|
|
||||||
this.interruptQueue = new UserInterruptQueue(this);
|
this.interruptQueue = new UserInterruptQueue(this);
|
||||||
|
|
||||||
this.clearMciCache = function() {
|
|
||||||
this.mciCache = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.defineProperty(this, 'currentTheme', {
|
Object.defineProperty(this, 'currentTheme', {
|
||||||
get : () => {
|
get : () => {
|
||||||
if (this.currentThemeConfig) {
|
if (this.currentThemeConfig) {
|
||||||
|
|
|
@ -21,6 +21,8 @@ function ClientTerminal(output) {
|
||||||
// convert line feeds such as \n -> \r\n
|
// convert line feeds such as \n -> \r\n
|
||||||
this.convertLF = true;
|
this.convertLF = true;
|
||||||
|
|
||||||
|
this.syncTermFontsEnabled = false;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Some terminal we handle specially
|
// Some terminal we handle specially
|
||||||
// They can also be found in this.env{}
|
// They can also be found in this.env{}
|
||||||
|
|
256
core/connect.js
256
core/connect.js
|
@ -12,6 +12,28 @@ const async = require('async');
|
||||||
|
|
||||||
exports.connectEntry = connectEntry;
|
exports.connectEntry = connectEntry;
|
||||||
|
|
||||||
|
const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
|
||||||
|
let giveUpTimer;
|
||||||
|
|
||||||
|
const done = function(err) {
|
||||||
|
client.removeListener('cursor position report', cprListener);
|
||||||
|
clearTimeout(giveUpTimer);
|
||||||
|
return cb(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cprListener = (pos) => {
|
||||||
|
cprHandler(pos);
|
||||||
|
return done(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.once('cursor position report', cprListener);
|
||||||
|
|
||||||
|
// give up after 2s
|
||||||
|
giveUpTimer = setTimeout( () => {
|
||||||
|
return done(Errors.General(failMessage));
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
function ansiDiscoverHomePosition(client, cb) {
|
function ansiDiscoverHomePosition(client, cb) {
|
||||||
//
|
//
|
||||||
// We want to find the home position. ANSI-BBS and most terminals
|
// We want to find the home position. ANSI-BBS and most terminals
|
||||||
|
@ -19,47 +41,34 @@ function ansiDiscoverHomePosition(client, cb) {
|
||||||
// think of home as 0,0. If this is the case, we need to offset
|
// think of home as 0,0. If this is the case, we need to offset
|
||||||
// our positioning to accommodate for such.
|
// our positioning to accommodate for such.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
if( !Config().term.checkAnsiHomePosition ) {
|
if( !Config().term.checkAnsiHomePosition ) {
|
||||||
// Skip (and assume 1,1) if the home position check is disabled.
|
// Skip (and assume 1,1) if the home position check is disabled.
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const done = (err) => {
|
withCursorPositionReport(
|
||||||
client.removeListener('cursor position report', cprListener);
|
client,
|
||||||
clearTimeout(giveUpTimer);
|
pos => {
|
||||||
return cb(err);
|
const [h, w] = pos;
|
||||||
};
|
|
||||||
|
|
||||||
const cprListener = function(pos) {
|
|
||||||
const h = pos[0];
|
|
||||||
const w = pos[1];
|
|
||||||
|
|
||||||
//
|
|
||||||
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
|
|
||||||
//
|
|
||||||
if(h > 1 || w > 1) {
|
|
||||||
client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values');
|
|
||||||
return done(Errors.UnexpectedState('Home position CPR expected to be 0,0, or 1,1'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(0 === h & 0 === w) {
|
|
||||||
//
|
//
|
||||||
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount
|
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
|
||||||
//
|
//
|
||||||
client.log.info('Setting CPR offset to 1');
|
if(h > 1 || w > 1) {
|
||||||
client.cprOffset = 1;
|
return client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values');
|
||||||
}
|
}
|
||||||
|
|
||||||
return done(null);
|
if(0 === h & 0 === w) {
|
||||||
};
|
//
|
||||||
|
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount
|
||||||
client.once('cursor position report', cprListener);
|
//
|
||||||
|
client.log.info('Setting CPR offset to 1');
|
||||||
const giveUpTimer = setTimeout( () => {
|
client.cprOffset = 1;
|
||||||
return done(Errors.General('Giving up on home position CPR'));
|
}
|
||||||
}, 3000); // 3s
|
},
|
||||||
|
'ANSI query home position timed out',
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
|
||||||
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
|
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
|
||||||
}
|
}
|
||||||
|
@ -82,56 +91,74 @@ function ansiAttemptDetectUTF8(client, cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let posStage = 1;
|
|
||||||
let initialPosition;
|
let initialPosition;
|
||||||
let giveUpTimer;
|
|
||||||
|
|
||||||
const giveUp = () => {
|
|
||||||
client.removeListener('cursor position report', cprListener);
|
|
||||||
clearTimeout(giveUpTimer);
|
|
||||||
return cb(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ASCIIPortion = ' Character encoding detection ';
|
const ASCIIPortion = ' Character encoding detection ';
|
||||||
|
|
||||||
const cprListener = (pos) => {
|
withCursorPositionReport(
|
||||||
switch(posStage) {
|
client,
|
||||||
case 1 :
|
pos => {
|
||||||
posStage = 2;
|
initialPosition = pos;
|
||||||
|
|
||||||
initialPosition = pos;
|
withCursorPositionReport(client,
|
||||||
clearTimeout(giveUpTimer);
|
pos => {
|
||||||
|
const [_, w] = pos;
|
||||||
giveUpTimer = setTimeout( () => {
|
const len = w - initialPosition[1];
|
||||||
return giveUp();
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
client.once('cursor position report', cprListener);
|
|
||||||
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
|
|
||||||
client.term.rawWrite(ansi.queryPos());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2 :
|
|
||||||
{
|
|
||||||
clearTimeout(giveUpTimer);
|
|
||||||
const len = pos[1] - initialPosition[1];
|
|
||||||
if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull
|
if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull
|
||||||
client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".');
|
client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".');
|
||||||
client.setTermType('ansi');
|
client.setTermType('ansi');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
return cb(null);
|
'Detect UTF-8 stage 2 timed out',
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
|
||||||
|
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
|
||||||
|
client.term.rawWrite(ansi.queryPos());
|
||||||
|
},
|
||||||
|
'Detect UTF-8 stage 1 timed out',
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
// bailing early
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
giveUpTimer = setTimeout( () => {
|
client.term.rawWrite(`${ansi.goHome()}${ansi.queryPos()}`);
|
||||||
return giveUp();
|
}
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
|
const ansiQuerySyncTermFontSupport = (client, cb) => {
|
||||||
|
// If we know it's SyncTERM, then good to go
|
||||||
|
if (client.term.termClient === 'cterm') {
|
||||||
|
client.term.syncTermFontsEnabled = true;
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some terminals do not properly ignore an unknown ESC sequence for
|
||||||
|
// setting SyncTERM style fonts. Some draw only a 'D' while others
|
||||||
|
// show more of the sequence
|
||||||
|
//
|
||||||
|
// If the cursor remains at 1,1, they are at least ignoring it, if not
|
||||||
|
// supporting it.
|
||||||
|
//
|
||||||
|
// We also need CPR support. Since all known terminals that support
|
||||||
|
// SyncTERM fonts also support CPR, this works out.
|
||||||
|
//
|
||||||
|
withCursorPositionReport(
|
||||||
|
client,
|
||||||
|
pos => {
|
||||||
|
const [_, w] = pos;
|
||||||
|
if (w === 1) {
|
||||||
|
// cursor didn't move
|
||||||
|
client.log.info('Client supports SyncTERM fonts or properly ignores unknown ESC sequence');
|
||||||
|
client.term.syncTermFontsEnabled = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'SyncTERM font detection timed out',
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
|
||||||
client.once('cursor position report', cprListener);
|
client.term.rawWrite(`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`);
|
||||||
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ansiQueryTermSizeIfNeeded(client, cb) {
|
function ansiQueryTermSizeIfNeeded(client, cb) {
|
||||||
|
@ -139,56 +166,43 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const done = function(err) {
|
withCursorPositionReport(
|
||||||
client.removeListener('cursor position report', cprListener);
|
client,
|
||||||
clearTimeout(giveUpTimer);
|
pos => {
|
||||||
return cb(err);
|
//
|
||||||
};
|
// If we've already found out, disregard
|
||||||
|
//
|
||||||
|
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cprListener = function(pos) {
|
//
|
||||||
//
|
// NetRunner for example gives us 1x1 here. Not really useful. Ignore
|
||||||
// If we've already found out, disregard
|
// values that seem obviously bad. Included in the set is the explicit
|
||||||
//
|
// 999x999 values we asked to move to.
|
||||||
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
|
//
|
||||||
return done(null);
|
const [h, w] = pos;
|
||||||
}
|
if(h < 10 || h === 999 || w < 10 || w === 999) {
|
||||||
|
return client.log.warn(
|
||||||
|
{ height : h, width : w },
|
||||||
|
'Ignoring ANSI CPR screen size query response due to non-sane values');
|
||||||
|
}
|
||||||
|
|
||||||
const h = pos[0];
|
client.term.termHeight = h;
|
||||||
const w = pos[1];
|
client.term.termWidth = w;
|
||||||
|
|
||||||
//
|
client.log.debug(
|
||||||
// NetRunner for example gives us 1x1 here. Not really useful. Ignore
|
{
|
||||||
// values that seem obviously bad. Included in the set is the explicit
|
termWidth : client.term.termWidth,
|
||||||
// 999x999 values we asked to move to.
|
termHeight : client.term.termHeight,
|
||||||
//
|
source : 'ANSI CPR'
|
||||||
if(h < 10 || h === 999 || w < 10 || w === 999) {
|
},
|
||||||
client.log.warn(
|
'Window size updated'
|
||||||
{ height : h, width : w },
|
);
|
||||||
'Ignoring ANSI CPR screen size query response due to non-sane values');
|
},
|
||||||
return done(Errors.Invalid('Term size <= 10 considered invalid'));
|
'No term size established by CPR within timeout',
|
||||||
}
|
cb
|
||||||
|
);
|
||||||
client.term.termHeight = h;
|
|
||||||
client.term.termWidth = w;
|
|
||||||
|
|
||||||
client.log.debug(
|
|
||||||
{
|
|
||||||
termWidth : client.term.termWidth,
|
|
||||||
termHeight : client.term.termHeight,
|
|
||||||
source : 'ANSI CPR'
|
|
||||||
},
|
|
||||||
'Window size updated'
|
|
||||||
);
|
|
||||||
|
|
||||||
return done(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
client.once('cursor position report', cprListener);
|
|
||||||
|
|
||||||
// give up after 2s
|
|
||||||
const giveUpTimer = setTimeout( () => {
|
|
||||||
return done(Errors.General('No term size established by CPR within timeout'));
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
// Start the process:
|
// Start the process:
|
||||||
// 1 - Ask to goto 999,999 -- a very much "bottom right" (generally 80x25 for example
|
// 1 - Ask to goto 999,999 -- a very much "bottom right" (generally 80x25 for example
|
||||||
|
@ -223,12 +237,11 @@ function connectEntry(client, nextMenu) {
|
||||||
[
|
[
|
||||||
function basicPrepWork(callback) {
|
function basicPrepWork(callback) {
|
||||||
term.rawWrite(ansi.queryDeviceAttributes(0));
|
term.rawWrite(ansi.queryDeviceAttributes(0));
|
||||||
|
term.write('\nDetecting client features...');
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function discoverHomePosition(callback) {
|
function discoverHomePosition(callback) {
|
||||||
ansiDiscoverHomePosition(client, () => {
|
return ansiDiscoverHomePosition(client, callback);
|
||||||
return callback(null); // we try to continue anyway
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
function queryTermSizeByNonStandardAnsi(callback) {
|
function queryTermSizeByNonStandardAnsi(callback) {
|
||||||
ansiQueryTermSizeIfNeeded(client, err => {
|
ansiQueryTermSizeIfNeeded(client, err => {
|
||||||
|
@ -254,7 +267,10 @@ function connectEntry(client, nextMenu) {
|
||||||
},
|
},
|
||||||
function checkUtf8IfNeeded(callback) {
|
function checkUtf8IfNeeded(callback) {
|
||||||
return ansiAttemptDetectUTF8(client, callback);
|
return ansiAttemptDetectUTF8(client, callback);
|
||||||
}
|
},
|
||||||
|
function querySyncTERMFontSupport(callback) {
|
||||||
|
return ansiQuerySyncTermFontSupport(client, callback);
|
||||||
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
prepareTerminal(term);
|
prepareTerminal(term);
|
||||||
|
|
|
@ -231,8 +231,6 @@ function SSHClient(clientConn) {
|
||||||
if(termHeight > 0 && termWidth > 0) {
|
if(termHeight > 0 && termWidth > 0) {
|
||||||
self.term.termHeight = termHeight;
|
self.term.termHeight = termHeight;
|
||||||
self.term.termWidth = termWidth;
|
self.term.termWidth = termWidth;
|
||||||
|
|
||||||
self.clearMciCache(); // term size changes = invalidate cache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) {
|
if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) {
|
||||||
|
|
|
@ -128,7 +128,7 @@ class TelnetClient {
|
||||||
const value = parseInt(getValue(what));
|
const value = parseInt(getValue(what));
|
||||||
if (value) {
|
if (value) {
|
||||||
this.term[what === 'ROWS' ? 'termHeight' : 'termWidth'] = value;
|
this.term[what === 'ROWS' ? 'termHeight' : 'termWidth'] = value;
|
||||||
this.clearMciCache();
|
|
||||||
this._logDebug(
|
this._logDebug(
|
||||||
{ [ what ] : value, source : 'NEW-ENVIRON' },
|
{ [ what ] : value, source : 'NEW-ENVIRON' },
|
||||||
'Window size updated'
|
'Window size updated'
|
||||||
|
@ -157,8 +157,6 @@ class TelnetClient {
|
||||||
this.term.env.ROWS = height;
|
this.term.env.ROWS = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearMciCache();
|
|
||||||
|
|
||||||
this._logDebug(
|
this._logDebug(
|
||||||
{ width, height, source : 'NAWS' },
|
{ width, height, source : 'NAWS' },
|
||||||
'Windows size updated'
|
'Windows size updated'
|
||||||
|
|
|
@ -199,7 +199,8 @@ exports.getModule = class TelnetBridgeModule extends MenuModule {
|
||||||
self.client.removeListener('key press', connectionKeyPressHandler);
|
self.client.removeListener('key press', connectionKeyPressHandler);
|
||||||
self.client.log.info(connectOpts, 'Telnet bridge connection established');
|
self.client.log.info(connectOpts, 'Telnet bridge connection established');
|
||||||
|
|
||||||
if(self.config.font) {
|
// put the font back how it was prior, if fonts are enabled
|
||||||
|
if(self.client.term.syncTermFontsEnabled && self.config.font) {
|
||||||
self.client.term.rawWrite(setSyncTermFontWithAlias(self.config.font));
|
self.client.term.rawWrite(setSyncTermFontWithAlias(self.config.font));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ GEM
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
mercenary (0.4.0)
|
mercenary (0.4.0)
|
||||||
minitest (5.15.0)
|
minitest (5.15.0)
|
||||||
nokogiri (1.13.1-x86_64-linux)
|
nokogiri (1.13.4-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
|
|
|
@ -58,7 +58,6 @@
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"uuid-parse": "1.1.0",
|
"uuid-parse": "1.1.0",
|
||||||
"ws": "7.4.3",
|
"ws": "7.4.3",
|
||||||
"xxhash": "^0.3.0",
|
|
||||||
"yazl": "^2.5.1",
|
"yazl": "^2.5.1",
|
||||||
"systeminformation" : "^4.27.5"
|
"systeminformation" : "^4.27.5"
|
||||||
},
|
},
|
||||||
|
|
95
yarn.lock
95
yarn.lock
|
@ -287,10 +287,10 @@ chardet@^0.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||||
|
|
||||||
chownr@^1.0.1:
|
chownr@^1.1.4:
|
||||||
version "1.1.1"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||||
integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
|
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||||
|
|
||||||
cli-cursor@^3.1.0:
|
cli-cursor@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
|
@ -710,12 +710,12 @@ fs-extra@^10.0.1:
|
||||||
jsonfile "^6.0.1"
|
jsonfile "^6.0.1"
|
||||||
universalify "^2.0.0"
|
universalify "^2.0.0"
|
||||||
|
|
||||||
fs-minipass@^1.2.5:
|
fs-minipass@^1.2.7:
|
||||||
version "1.2.5"
|
version "1.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
|
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
|
||||||
integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==
|
integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass "^2.2.1"
|
minipass "^2.6.0"
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
|
@ -1155,37 +1155,32 @@ mimic-fn@^2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@0.0.8:
|
|
||||||
version "0.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
|
||||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6:
|
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
minipass@^2.2.1, minipass@^2.3.3:
|
minipass@^2.6.0, minipass@^2.9.0:
|
||||||
version "2.3.4"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
|
||||||
integrity sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==
|
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.1.2"
|
safe-buffer "^5.1.2"
|
||||||
yallist "^3.0.0"
|
yallist "^3.0.0"
|
||||||
|
|
||||||
minizlib@^1.1.0:
|
minizlib@^1.3.3:
|
||||||
version "1.1.0"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
|
||||||
integrity sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==
|
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass "^2.2.1"
|
minipass "^2.9.0"
|
||||||
|
|
||||||
mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
|
mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||||
version "0.5.1"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "0.0.8"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
moment@^2.19.3:
|
moment@^2.19.3:
|
||||||
version "2.29.1"
|
version "2.29.1"
|
||||||
|
@ -1231,7 +1226,7 @@ nan@^2.10.0:
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
|
||||||
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
|
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
|
||||||
|
|
||||||
nan@^2.12.1, nan@^2.13.2, nan@^2.14.0:
|
nan@^2.12.1, nan@^2.14.0:
|
||||||
version "2.14.0"
|
version "2.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||||
|
@ -1612,7 +1607,12 @@ rxjs@^7.5.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
safe-buffer@^5.1.2, safe-buffer@^5.2.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
|
||||||
|
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||||
|
@ -1827,17 +1827,17 @@ systeminformation@^4.27.5:
|
||||||
integrity sha512-33+lQwlLxXoxy0o9WLOgw8OjbXeS3Jv+pSl+nxKc2AOClBI28HsdRPpH0u9Xa9OVjHLT9vonnOMw1ug7YXI0dA==
|
integrity sha512-33+lQwlLxXoxy0o9WLOgw8OjbXeS3Jv+pSl+nxKc2AOClBI28HsdRPpH0u9Xa9OVjHLT9vonnOMw1ug7YXI0dA==
|
||||||
|
|
||||||
tar@^4:
|
tar@^4:
|
||||||
version "4.4.6"
|
version "4.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
|
||||||
integrity sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==
|
integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr "^1.0.1"
|
chownr "^1.1.4"
|
||||||
fs-minipass "^1.2.5"
|
fs-minipass "^1.2.7"
|
||||||
minipass "^2.3.3"
|
minipass "^2.9.0"
|
||||||
minizlib "^1.1.0"
|
minizlib "^1.3.3"
|
||||||
mkdirp "^0.5.0"
|
mkdirp "^0.5.5"
|
||||||
safe-buffer "^5.1.2"
|
safe-buffer "^5.2.1"
|
||||||
yallist "^3.0.2"
|
yallist "^3.1.1"
|
||||||
|
|
||||||
telnet-socket@^0.2.3:
|
telnet-socket@^0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
|
@ -2020,17 +2020,10 @@ xtend@~4.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||||
|
|
||||||
xxhash@^0.3.0:
|
yallist@^3.0.0, yallist@^3.1.1:
|
||||||
version "0.3.0"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.3.0.tgz#d20893a62c5b0f7260597dd55859b12a1e02c559"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||||
integrity sha512-1ud2yyPiR1DJhgyF1ZVMt+Ijrn0VNS/wzej1Z8eSFfkNfRPp8abVZNV2u9tYy9574II0ZayZYZgJm8KJoyGLCw==
|
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||||
dependencies:
|
|
||||||
nan "^2.13.2"
|
|
||||||
|
|
||||||
yallist@^3.0.0, yallist@^3.0.2:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
|
|
||||||
integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=
|
|
||||||
|
|
||||||
yazl@^2.5.1:
|
yazl@^2.5.1:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
|
|
Loading…
Reference in New Issue