diff --git a/core/string_format.js b/core/string_format.js index d6f6431a..e752064b 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -291,6 +291,7 @@ function getValue(obj, path) { module.exports = function format(fmt, obj) { const re = REGEXP_BASIC_FORMAT; + re.lastIndex = 0; // reset from prev let match; let pos; diff --git a/core/string_util.js b/core/string_util.js index 407f8046..ab543a4e 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -195,30 +195,37 @@ function stringFromNullTermBuffer(buf, encoding) { return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8'); } -// :TODO: Add other codes from ansi_escape_parser -const ANSI_REGEXP = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; -const PIPE_REGEXP = /\|[A-Z\d]{2}/g; -const ANSI_OR_PIPE_REGEXP = new RegExp(ANSI_REGEXP.source + '|' + PIPE_REGEXP.source, 'g'); +const PIPE_REGEXP = /(\|[A-Z\d]{2})/g; +const ANSI_REGEXP = /[\u001b\u009b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/g; +const ANSI_OR_PIPE_REGEXP = new RegExp(PIPE_REGEXP.source + '|' + ANSI_REGEXP.source, 'g'); // // Similar to substr() but works with ANSI/Pipe code strings // function renderSubstr(str, start, length) { - start = start || 0; - length = Math.max(0, (length || str.length - start) - 1); + // shortcut for empty strings + if(0 === str.length) { + return str; + } - const re = ANSI_REGEXP; - let pos; + start = start || 0; + length = length || str.length - start; + + const re = ANSI_OR_PIPE_REGEXP; + re.lastIndex = 0; // we recycle the obj; must reset! + + let pos = 0; let match; let out = ''; let renderLen = 0; + let s; do { pos = re.lastIndex; match = re.exec(str); if(match) { if(match.index > pos) { - const s = str.slice(pos + start, match.index - (Math.min(0, length - renderLen))); + s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); start = 0; // start offset applies only once out += s; renderLen += s.length; @@ -230,19 +237,53 @@ function renderSubstr(str, start, length) { // remainder if(pos + start < str.length && renderLen < length) { - out += str.slice(pos + start, pos + Math.max(0, length - renderLen)); + out += str.slice(pos + start, (pos + start + (length - renderLen))); + //out += str.slice(pos + start, Math.max(1, pos + (length - renderLen - 1))); } return out; } // -// Method to return the "rendered" length taking into account -// Pipe and ANSI color codes. Note that currently ANSI *movement* -// codes are not considred! +// Method to return the "rendered" length taking into account Pipe and ANSI color codes. // -function renderStringLength(str) { - return str.replace(ANSI_OR_PIPE_REGEXP, '').length; +// We additionally account for ANSI *forward* movement ESC sequences +// in the form of ESC[C where is the "go forward" character count. +// +// See also https://github.com/chalk/ansi-regex/blob/master/index.js +// +function renderStringLength(s) { + let m; + let pos; + let len = 0; + + const re = ANSI_OR_PIPE_REGEXP; + re.lastIndex = 0; // we recycle the rege; reset + + // + // Loop counting only literal (non-control) sequences + // paying special attention to ESC[C which means forward + // + do { + pos = re.lastIndex; + m = re.exec(s); + + if(m) { + if(m.index > pos) { + len += s.slice(pos, m.index).length; + } + + if('C' === m[3]) { // ESC[C is foward/right + len += parseInt(m[2], 10) || 0; + } + } + } while(0 !== re.lastIndex); + + if(pos < s.length) { + len += s.slice(pos).length; + } + + return len; } @@ -267,7 +308,7 @@ function cleanControlCodes(input) { pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; m = REGEXP_ANSI_CONTROL_CODES.exec(input); - if(null !== m) { + if(m) { if(m.index > pos) { cleaned += input.slice(pos, m.index); } diff --git a/core/word_wrap.js b/core/word_wrap.js index d859b08d..39439cce 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,8 +1,9 @@ /* jslint node: true */ 'use strict'; -var assert = require('assert'); -var _ = require('lodash'); +var assert = require('assert'); +var _ = require('lodash'); +const renderStringLength = require('./string_util.js').renderStringLength; exports.wordWrapText = wordWrapText2; @@ -15,22 +16,26 @@ const SPACE_CHARS = [ const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); +/* // // ANSI & pipe codes we indend to strip // // See also https://github.com/chalk/ansi-regex/blob/master/index.js // // :TODO: Consolidate this, regexp's in ansi_escape_parser, and strutil. Need more complete set that includes common standads, bansi, and cterm.txt +// renderStringLength() from strUtil does not account for ESC[C (e.g. go forward) const REGEXP_CONTROL_CODES = /(\|[\d]{2})|(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g; function getRenderLength(s) { let m; let pos; let len = 0; + + REGEXP_CONTROL_CODES.lastIndex = 0; // reset // // Loop counting only literal (non-control) sequences - // paying special attention to ESC[C which means foward + // paying special attention to ESC[C which means forward // do { pos = REGEXP_CONTROL_CODES.lastIndex; @@ -53,6 +58,7 @@ function getRenderLength(s) { return len; } +*/ function wordWrapText2(text, options) { assert(_.isObject(options)); @@ -79,7 +85,7 @@ function wordWrapText2(text, options) { function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { - renderLen = getRenderLength(w); + renderLen = renderStringLength(w); if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index b84ced5c..1397060c 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -206,8 +206,8 @@ messageAreaMessageList: { config: { - listFormat: "|00|15{msgNum:>4} |03{subject:<29.29} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}" - focusListFormat: "|00|19|15{msgNum:>4} {subject:<29.29} {fromUserName:<20.20} {ts} {newIndicator}" + listFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}" + focusListFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" dateTimeFormat: ddd MMM Do } mci: { @@ -260,8 +260,8 @@ mailMenuInbox: { config: { - listFormat: "|00|15{msgNum:>4} |03{subject:<29.29} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}" - focusListFormat: "|00|19|15{msgNum:>4} {subject:<29.29} {fromUserName:<20.20} {ts} {newIndicator}" + listFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}" + focusListFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" dateTimeFormat: ddd MMM Do } mci: { @@ -467,8 +467,8 @@ newScanMessageList: { config: { - listFormat: "|00|15 {msgNum:<5.5}|03{subject:<29.29} |15{fromUserName:<20.20} {ts}" - focusListFormat: "|00|19> |15{msgNum:<5.5}{subject:<29.29} {fromUserName:<20.20} {ts}" + listFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}" + focusListFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}" dateTimeFormat: ddd MMM Do } mci: {