if (!_G.TAUT) _G.TAUT = {}; let help = {} //////////////////////////////////////////////////////////////////////////////////////////////////// /* Tags: - print the text in emphasis colour (colVoiceHdr aka 230) - centre the line. If the line spans multiple lines, centre each line - align right - align left - create virtual typesetting box. Left anchor: where the text cursor is. Right anchor: end of the line µtone; - replace with the brand string (Microtone) &bul; - replace with bullet (\u00F9) &ddot; - replace with double-dot (\u008419u) &mdot; - replace with BIGDOT (\u00FA) &updn; - up-down arrow (\u008418u) &udlr; - four direction arrow (\u008428u\u008429u) &keyoffsym; - pattern view key-off symbol (\u00A0\u00B1\u00B1\u00A1) ¬ecutsym; - pattern view note-cut symbol (\u00A4\u00A4\u00A4\u00A4) &demisharp; ♯ &sesquisharp; &doublesharp; &triplesharp; &quadsharp; &demiflat; ♭ &sesquiflat; &doubleflat; &tripleflat; &quadflat; &accuptick; &accdntick; &accupup; &accdndn;   - nonbreakable space (only meaningful for typesetters) ­ - soft hyphen (only meaningful for typesetters) default alignment: fully justified */ let helpNotation = `CONTROL NOTATION \u00B7${'\u00B8'.repeat(16)}\u00B9 µtone; shortcuts differentiate normal and shifted shortcuts. &bul;a&ddot;z : alphabet without shift-in &bul;A&ddot;Z : alphabet with shift-in &bul;^q : hit 'q' with control key &bul;^Q : hit 'q' with control and shift key ` //////////////////////////////////////////////////////////////////////////////////////////////////// let helpJam = `NOTE JAMMING \u00B7${'\u00B8'.repeat(12)}\u00B9 Push keys to play or insert notes.  w e   t y u a s d f g h j k ` //////////////////////////////////////////////////////////////////////////////////////////////////// let helpCommon = `COMMON CONTROLS \u00B7${'\u00B8'.repeat(15)}\u00B9 &bul;! : show this help message &bul;Y : plays the entire song from the current cue &bul;U : plays the current cue then stop &bul;I : plays the current row &bul;O : stops the playback &bul;tab : switchs forward a tab &bul;TAB : switchs backward a tab &bul;q : closes µtone; ` //////////////////////////////////////////////////////////////////////////////////////////////////// let helpTimeline = `TIMELINE VIEW \u00B7${'\u00B8'.repeat(13)}\u00B9 Timeline has two distinct modes: view and edit mode. Two modes are toggled using the space bar.  VIEW MODE \u00B7${'\u00B8'.repeat(9)}\u00B9 &bul;Note jamming : plays the note &bul;&udlr; : moves the viewing cursor by voices and rows &bul;pg&updn; : goes to previous/next cue &bul;W&mdot;E&mdot;R : toggles timeline view mode. W-most detailed, R-most abridged &bul;n : toggles soloing of the selected voice &bul;m : toggles muting of the selected voice &bul;[&mdot;] : changes tick rate of playhead  EDIT MODE \u00B7${'\u00B8'.repeat(9)}\u00B9 &bul;Note jamming : (note column) inserts the note &bul;{&mdot;} : (note column) lowers/raises a note by one octave (or period) &bul;[&mdot;] : (note column) lowers/raises a note by one unit &bul;z : (note column) inserts a key-off &keyoffsym; &bul;x : (note column) inserts a note-cut ¬ecutsym; &bul;. : clears fields &bul;bksp : deletes one character on the selected column &bul;0&ddot;9 a&ddot;f : inserts a (hexa)decimal number &bul;0&ddot;9 a&ddot;z : (fx column) inserts an effect &bul;^&mdot;v : (volume column) slide up/down &bul;<&mdot;>: (panning column) slide left/right &bul;-&mdot;= : (vol/pan col) fine slide down/up &bul;&udlr; : moves the viewing cursor by columns and rows &bul;pg&updn; : goes to previous/next cue  ACCIDENTALS \u00B7${'\u00B8'.repeat(11)}\u00B9 &demisharp; ♯ &doublesharp; &triplesharp; &quadsharp; &demiflat; ♭ &doubleflat; &tripleflat;  &accuptick;  &accupup;  &accdntick;  &accdndn; C  c  cx x  xx B  b  bb bbb ^  ^^ v  vv  GLOBAL EDIT \u00B7${'\u00B8'.repeat(11)}\u00B9 &bul;Q : retunes current song into different tuning and strategy. In general, nearest-note works best for macrotonals, nearest-harmonic and nearest-delta works best for highly microtonals (31+); 17- and 19-TET takes nearest-harmonic pretty well, while 22-TET seem to only benefit from the nearest-note ` let helpProjectFlags = `MIXER FLAGS \u00B7${'\u00B8'.repeat(11)}\u00B9 Mixer flags define how should the mixer behave.  TONE MODE \u00B7${'\u00B8'.repeat(9)}\u00B9 &bul;Linear pitch : pitch shift effects operate on linear pitch scale. The default and recommended setting for a new project &bul;Amiga pitch : pitch shift effects operate on Amiga period scale. Backwards compatible setting for MOD/S3M/XM/IT formats &bul;Linear freq : pitch shift effects operate on linear frequency scale. Backwards compatible setting for MONOTONE format  INTERPOLATION \u00B7${'\u00B8'.repeat(13)}\u00B9 &bul;Default : three-tap fast sinc interpolation. The default and recommended setting for a new project &bul;None : zeroth-order hold &bul;A500 : emulates what Paula chip of Amiga 500 does. S 0x00 effects only work with this and Amiga 1200 mode &bul;A1200 : emulates what Paula chip of Amiga 1200 does &bul;SNES : four-tap gaussian interpolation used by SNES &bul;DPCM : simulates Differential Pulse Code Modulation used by NES ` //////////////////////////////////////////////////////////////////////////////////////////////////// // assemble help text pieces to complete help message const SCRW = con.getmaxyx()[1] const HRULE = '\u00B4\u00B5'.repeat((_G.TAUT.HELPMSG_WIDTH) >>> 1) + '\n' // Display-command palette. taut.js's popup uses (HELP_COL_TEXT on background) as the // default colour pair, so embedded `\x1B[38;5;Nm` codes switch foreground only. const HELP_COL_TEXT = 239 // popup body default (== colWHITE) const HELP_COL_EMPH = 230 // ... highlight (== colVoiceHdr) const HELP_COL_BRAND = 211 // first half of "Microtone" const HELP_COL_BRAND_DIM = 239 // second half of "Microtone" const fgEsc = (n) => `\x1B[38;5;${n}m` const ESC_DEFAULT = fgEsc(HELP_COL_TEXT) const ESC_EMPH = fgEsc(HELP_COL_EMPH) const MICROTONE = `${fgEsc(HELP_COL_BRAND)}Micro${fgEsc(HELP_COL_BRAND_DIM)}tone${ESC_DEFAULT}` // Replace &xxx; entities with their final printable representations. function expandEntities(s) { return s .replaceAll('µtone;', MICROTONE) .replaceAll('&bul;', '\u00F9') .replaceAll('&ddot;', '\u008419u') .replaceAll('&mdot;', '\u00FA') .replaceAll('&updn;', '\u008418u') .replaceAll('&udlr;', '\u008428u\u008429u') .replaceAll('&keyoffsym;', '\u00A0\u00B1\u00B1\u00A1') .replaceAll('¬ecutsym;', '\u00A4\u00A4\u00A4\u00A4') .replaceAll(' ', '\u007F') .replaceAll('­', '') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('&demisharp;', '\u0080\u0081') .replaceAll('♯', '\u0082\u0083') .replaceAll('&sesquisharp;', '\u0084132u\u0085') .replaceAll('&doublesharp;', '\u0086\u0087') .replaceAll('&triplesharp;', '\u0088\u0089') .replaceAll('&quadsharp;', '\u008A\u008B') .replaceAll('&demiflat;', '\u008C\u008D') .replaceAll('♭', '\u008E\u008F') .replaceAll('&sesquiflat;', '\u0090\u0091') .replaceAll('&doubleflat;', '\u0092\u0093') .replaceAll('&tripleflat;', '\u0094\u0095') .replaceAll('&quadflat;', '\u0096\u0097') .replaceAll('&accuptick;', '\u009A') .replaceAll('&accdntick;', '\u009B') .replaceAll('&accupup;', '\u009C') .replaceAll('&accdndn;', '\u009D') } // Tokenise a (post-entity-expansion) line. Returns an array of: // {type:'word', text:String, w:int} - non-breakable run of visible chars (may carry ANSI escapes) // {type:'sp'} - a single soft space (eligible for break/expansion) // {type:'anchor', open:Boolean} - / markers (zero width) // // Width accounting: // - ANSI escapes (`\x1B[...m`) : 0 visible chars // - TSVM unicode escapes (`„..u`) : 1 visible char // - non-breaking space ( ) : 1 visible char (consumed as part of a word) // - soft hyphen (­) : dropped (not implemented as a break point) // - everything else : 1 visible char function tokenise(line) { const tokens = [] let buf = '' let bufW = 0 let i = 0 const flushWord = () => { if (buf.length > 0) { tokens.push({type: 'word', text: buf, w: bufW}) buf = '' bufW = 0 } } while (i < line.length) { // inline tags (case-sensitive for , case-insensitive for ) if (line.slice(i, i + 3) === '') { buf += ESC_EMPH; i += 3; continue } if (line.slice(i, i + 4) === '') { buf += ESC_DEFAULT; i += 4; continue } const head3 = line.slice(i, i + 3).toLowerCase() const head4 = line.slice(i, i + 4).toLowerCase() if (head3 === '') { flushWord(); tokens.push({type: 'anchor', open: true}); i += 3; continue } if (head4 === '') { flushWord(); tokens.push({type: 'anchor', open: false}); i += 4; continue } const c = line[i] const cc = line.charCodeAt(i) if (cc === 0x1B) { // pre-existing ANSI escape - copy verbatim, zero visible width const m = line.indexOf('m', i) const end = (m < 0) ? line.length : m + 1 buf += line.slice(i, end) i = end } else if (cc === 0x84) { // TSVM „u escape - copy verbatim, one visible char const u = line.indexOf('u', i) const end = (u < 0) ? line.length : u + 1 buf += line.slice(i, end) bufW += 1 i = end } else if (c === ' ') { flushWord() tokens.push({type: 'sp'}) i += 1 } else if (cc === 0x00AD) { // soft hyphen: drop (no break-point handling for now) i += 1 } else { buf += c bufW += 1 i += 1 } } flushWord() return tokens } // Build wrapped lines from a token stream then format each one according to alignment. // Returns an array of strings, each exactly `width` visible chars wide (padded with // trailing spaces) so the caller can blit them without further math. function wrapAndAlign(tokens, width, alignment) { const lines = [] // each: {tokens, indent, contentW} let curTokens = [] let curW = 0 let curIndent = 0 let nextIndent = 0 // indent the *next* flushed line should use const flushLine = () => { // strip trailing soft spaces while (curTokens.length > 0 && curTokens[curTokens.length - 1].type === 'sp') { curTokens.pop() curW -= 1 } lines.push({tokens: curTokens, indent: curIndent, contentW: curW}) curTokens = [] curW = 0 curIndent = nextIndent } for (const tok of tokens) { if (tok.type === 'anchor') { // anchor opens at the current visible column (accounting for indent) if (tok.open) nextIndent = curIndent + curW else nextIndent = 0 continue } if (tok.type === 'sp') { // ignore leading soft spaces on a fresh line if (curW === 0) continue // hard wrap if the line is already at the right edge if (curIndent + curW + 1 > width) { flushLine(); continue } curTokens.push(tok) curW += 1 continue } // word const tw = tok.w if (curIndent + curW + tw > width) { flushLine() // word too wide for the wrapped line: emit it on its own row (possibly clipped by terminal) if (curIndent + tw > width) { curTokens.push(tok) curW += tw flushLine() continue } } curTokens.push(tok) curW += tw } if (curTokens.length > 0 || lines.length === 0) flushLine() return lines.map((line, i) => formatLine(line, width, alignment, i === lines.length - 1)) } function formatLine(line, totalWidth, alignment, isLast) { if (line.tokens.length === 0) return ' '.repeat(totalWidth) const indent = ' '.repeat(line.indent) const remaining = totalWidth - line.indent - line.contentW const pad = (n) => (n > 0) ? ' '.repeat(n) : '' const flatText = () => line.tokens.map(t => (t.type === 'sp') ? ' ' : t.text).join('') if (alignment === 'c') { const left = remaining >> 1 return indent + pad(left) + flatText() + pad(remaining - left) } if (alignment === 'r') return indent + pad(remaining) + flatText() if (alignment === 'l') return indent + flatText() + pad(remaining) // justified: only expand spaces when there's slack and we're not on the // last (or single) wrapped line if (isLast || remaining <= 0) return indent + flatText() + pad(remaining) const spaceCount = line.tokens.reduce((n, t) => n + (t.type === 'sp' ? 1 : 0), 0) if (spaceCount === 0) return indent + flatText() + pad(remaining) const baseExtra = (remaining / spaceCount) | 0 let leftover = remaining - baseExtra * spaceCount let out = indent for (const tok of line.tokens) { if (tok.type === 'sp') { const extra = baseExtra + (leftover > 0 ? 1 : 0) if (leftover > 0) leftover -= 1 out += ' '.repeat(1 + extra) } else { out += tok.text } } return out } // Process a single source line: peel a leading // alignment tag (if present), // strip its matching close tag, then tokenise + wrap. function typesetSourceLine(line, width) { if (line.length === 0) return [' '.repeat(width)] let alignment = 'j' // justified default const startMatch = line.match(/^<([crl])>/i) if (startMatch) { alignment = startMatch[1].toLowerCase() line = line.slice(startMatch[0].length) const closeRe = new RegExp(`$`, 'i') line = line.replace(closeRe, '') } const tokens = tokenise(line) return wrapAndAlign(tokens, width, alignment) } function typesetText(text, width) { text = expandEntities(text) const out = [] for (const srcLine of text.split('\n')) { for (const outLine of typesetSourceLine(srcLine, width)) out.push(outLine) } return out } function typeset(text, customWidth) { let typesetWidth = customWidth if (typesetWidth === undefined) typesetWidth = _G.TAUT.HELPMSG_WIDTH if (typesetWidth === undefined) { const currentPosX = con.getyx()[1] // 1-indexed typesetWidth = SCRW - currentPosX + 1 } return typesetText(text, typesetWidth) } let helpMessages = [ // index: taut.js PANEL_NAMES /* Timeline */[helpJam, helpTimeline, helpCommon, helpNotation].join(HRULE), /* Cues */[helpCommon, helpNotation].join(HRULE), // placeholder /* Patterns */[helpCommon, helpNotation].join(HRULE), // placeholder /* Samples */[helpCommon, helpNotation].join(HRULE), // placeholder /* Instruments */[helpCommon, helpNotation].join(HRULE), // placeholder /* Project */[helpProjectFlags, helpCommon, helpNotation].join(HRULE), // placeholder /* File */[helpCommon, helpNotation].join(HRULE), // placeholder ] help.MSG_BY_TABS = helpMessages.map(it => typeset(it)) help.typeset = typeset help.COL_TEXT = HELP_COL_TEXT help.COL_EMPH = HELP_COL_EMPH if (!_G.TAUT.HELPMSG) _G.TAUT.HELPMSG=help;