SyncTERM Font Support Detection

* Attempt to detect SyncTERM font support using DSR/CPR
* If a terminal doesn't support DSR/CPR, assume it doesn't support SyncTERM fonts, either.
* Support is also disabled if term simply doesn't ignore the ESC sequence, either
This commit is contained in:
Bryan Ashby 2022-04-25 21:09:57 -06:00
parent ac010f2ca4
commit 15c3c87500
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
5 changed files with 164 additions and 143 deletions

View File

@ -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 = {

View File

@ -351,12 +351,13 @@ function display(client, art, options, cb) {
}); });
let initSeq = ''; let initSeq = '';
if (client.term.syncTermFontsEnabled) {
if(options.font) { if(options.font) {
initSeq = ansi.setSyncTermFontWithAlias(options.font); initSeq = ansi.setSyncTermFontWithAlias(options.font);
} else if(options.sauce) { } else if(options.sauce) {
let fontName = getFontNameFromSAUCE(options.sauce); let fontName = getFontNameFromSAUCE(options.sauce);
if(fontName) { if(fontName) {
fontName = ansi.getSyncTERMFontFromAlias(fontName); fontName = ansi.getSyncTermFontFromAlias(fontName);
} }
// //
@ -367,7 +368,8 @@ function display(client, art, options, cb) {
// //
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);
}
} }
} }

View File

@ -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{}

View File

@ -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,29 +41,21 @@ 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 // We expect either 0,0, or 1,1. Anything else will be filed as bad data
// //
if(h > 1 || w > 1) { if(h > 1 || w > 1) {
client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); return 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) { if(0 === h & 0 === w) {
@ -51,15 +65,10 @@ function ansiDiscoverHomePosition(client, cb) {
client.log.info('Setting CPR offset to 1'); client.log.info('Setting CPR offset to 1');
client.cprOffset = 1; client.cprOffset = 1;
} }
},
return done(null); 'ANSI query home position timed out',
}; cb
);
client.once('cursor position report', cprListener);
const giveUpTimer = setTimeout( () => {
return done(Errors.General('Giving up on home position CPR'));
}, 3000); // 3s
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;
clearTimeout(giveUpTimer);
giveUpTimer = setTimeout( () => { withCursorPositionReport(client,
return giveUp(); pos => {
}, 2000); const [_, w] = pos;
const len = w - initialPosition[1];
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');
} }
},
'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);
} }
}
);
client.term.rawWrite(`${ansi.goHome()}${ansi.queryPos()}`);
}
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); return cb(null);
} }
};
giveUpTimer = setTimeout( () => { // Some terminals do not properly ignore an unknown ESC sequence for
return giveUp(); // setting SyncTERM style fonts. Some draw only a 'D' while others
}, 2000); // 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.term.rawWrite(`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`);
client.once('cursor position report', cprListener);
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
} }
function ansiQueryTermSizeIfNeeded(client, cb) { function ansiQueryTermSizeIfNeeded(client, cb) {
@ -139,33 +166,26 @@ 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);
};
const cprListener = function(pos) {
// //
// If we've already found out, disregard // If we've already found out, disregard
// //
if(client.term.termHeight > 0 || client.term.termWidth > 0) { if(client.term.termHeight > 0 || client.term.termWidth > 0) {
return done(null); return;
} }
const h = pos[0];
const w = pos[1];
// //
// NetRunner for example gives us 1x1 here. Not really useful. Ignore // NetRunner for example gives us 1x1 here. Not really useful. Ignore
// values that seem obviously bad. Included in the set is the explicit // values that seem obviously bad. Included in the set is the explicit
// 999x999 values we asked to move to. // 999x999 values we asked to move to.
// //
const [h, w] = pos;
if(h < 10 || h === 999 || w < 10 || w === 999) { if(h < 10 || h === 999 || w < 10 || w === 999) {
client.log.warn( return client.log.warn(
{ height : h, width : w }, { height : h, width : w },
'Ignoring ANSI CPR screen size query response due to non-sane values'); 'Ignoring ANSI CPR screen size query response due to non-sane values');
return done(Errors.Invalid('Term size <= 10 considered invalid'));
} }
client.term.termHeight = h; client.term.termHeight = h;
@ -179,16 +199,10 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
}, },
'Window size updated' 'Window size updated'
); );
},
return done(null); 'No term size established by CPR within timeout',
}; cb
);
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);

View File

@ -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));
} }