From d51c9712425690151f48a182cf3655bf14ce7d39 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Tue, 24 Aug 2021 06:44:03 +1200 Subject: [PATCH] Improve performance of the `.clear()` method (#182) Co-authored-by: moofoo --- index.js | 12 +- package.json | 1 + test.js | 568 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 579 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 1f95e78..fca892b 100644 --- a/index.js +++ b/index.js @@ -142,6 +142,7 @@ class Ora { } this._indent = indent; + this.updateLineCount(); } _updateInterval(interval) { @@ -215,7 +216,7 @@ class Ora { const columns = this.stream.columns || 80; const fullPrefixText = this.getFullPrefixText(this.prefixText, '-'); this.lineCount = 0; - for (const line of stripAnsi(fullPrefixText + '--' + this[TEXT]).split('\n')) { + for (const line of stripAnsi(' '.repeat(this.indent) + fullPrefixText + '--' + this[TEXT]).split('\n')) { this.lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns)); } } @@ -264,15 +265,22 @@ class Ora { return this; } + this.stream.cursorTo(0); + for (let i = 0; i < this.linesToClear; i++) { if (i > 0) { this.stream.moveCursor(0, -1); } - this.stream.clearLine(); + this.stream.clearLine(1); + } + + if (this.indent || this.lastIndent !== this.indent) { this.stream.cursorTo(this.indent); } + this.lastIndent = this.indent; + this.linesToClear = 0; return this; diff --git a/package.json b/package.json index aca2c2b..323c489 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@types/node": "^14.14.35", "ava": "^2.4.0", "get-stream": "^6.0.0", + "transform-tty": "^1.0.11", "tsd": "^0.14.0", "xo": "^0.38.2" } diff --git a/test.js b/test.js index e0b8f2a..a2a87d9 100644 --- a/test.js +++ b/test.js @@ -3,6 +3,8 @@ import getStream from 'get-stream'; import test from 'ava'; import stripAnsi from 'strip-ansi'; import Ora from './index.js'; +import TransformTTY from 'transform-tty'; +import cliSpinners from 'cli-spinners'; const spinnerCharacter = process.platform === 'win32' ? '-' : 'โ ‹'; const noop = () => {}; @@ -365,6 +367,27 @@ test('indent option throws', t => { }, 'The `indent` option must be an integer from 0 and up'); }); +test('handles wrapped lines when length of indent + text is greater than columns', t => { + const stream = getPassThroughStream(); + stream.isTTY = true; + stream.columns = 20; + + const spinner = new Ora({ + stream, + text: 'foo', + color: false, + isEnabled: true + }); + + spinner.render(); + + spinner.text = '0'.repeat(spinner.stream.columns - 5); + spinner.indent = 15; + spinner.render(); + + t.is(spinner.lineCount, 2); +}); + test('.stopAndPersist() with prefixText', macro, spinner => { spinner.stopAndPersist({symbol: '@', text: 'foo'}); }, /bar @ foo\n$/, {prefixText: 'bar'}); @@ -384,3 +407,548 @@ test('.stopAndPersist() with manual empty prefixText', macro, spinner => { test('.stopAndPersist() with dynamic prefixText', macro, spinner => { spinner.stopAndPersist({symbol: '&', prefixText: () => 'babeee', text: 'yorkie'}); }, /babeee & yorkie\n$/, {prefixText: () => 'babeee'}); + +// New clear method tests + +const currentClearMethod = transFormTTY => { + const spinner = new Ora({ + text: 'foo', + color: false, + isEnabled: true, + stream: transFormTTY, + spinner: { + frames: ['-'] + } + }); + + let firstIndent = true; + + spinner.clear = function () { + if (!this.isEnabled || !this.stream.isTTY) { + return this; + } + + for (let i = 0; i < this.linesToClear; i++) { + if (i > 0) { + this.stream.moveCursor(0, -1); + } + + this.stream.clearLine(); + this.stream.cursorTo(this.indent); + } + + // It's too quick to be noticeable, but indent doesn't get applied + // for the first render if linesToClear = 0. New clear method + // doesn't have this issue, since it's called outside of the loop + if (this.linesToClear === 0 && firstIndent && this.indent) { + this.stream.cursorTo(this.indent); + firstIndent = false; + } + + this.linesToClear = 0; + + return this; + }.bind(spinner); + + return spinner; +}; + +test.serial('new clear method test, basic', t => { + const transformTTY = new TransformTTY({crlf: true}); + transformTTY.addSequencer(); + transformTTY.addSequencer(null, true); + /* + If the frames from this sequence differ from the previous sequence, + it means the spinner.clear method has failed to fully clear output between calls to render + */ + + const currentClearTTY = new TransformTTY({crlf: true}); + currentClearTTY.addSequencer(); + + const currentOra = currentClearMethod(currentClearTTY); + + const spinner = new Ora({ + text: 'foo', + color: false, + isEnabled: true, + stream: transformTTY, + spinner: { + frames: ['-'] + } + }); + + currentOra.render(); + spinner.render(); + + currentOra.text = 'bar'; + currentOra.indent = 5; + currentOra.render(); + + spinner.text = 'bar'; + spinner.indent = 5; + spinner.render(); + + currentOra.text = 'baz'; + currentOra.indent = 10; + currentOra.render(); + + spinner.text = 'baz'; + spinner.indent = 10; + spinner.render(); + + currentOra.succeed('boz?'); + + spinner.succeed('boz?'); + + const [sequenceString, clearedSequenceString] = transformTTY.getSequenceStrings(); + const [frames, clearedFrames] = transformTTY.getFrames(); + + t.is(sequenceString, ' โœ” boz?\n'); + t.is(sequenceString, clearedSequenceString); + + t.deepEqual(clearedFrames, ['- foo', ' - bar', ' - baz', ' โœ” boz?\n']); + t.deepEqual(frames, clearedFrames); + + const currentString = currentClearTTY.getSequenceStrings(); + + t.is(currentString, ' โœ” boz?\n'); + + const currentFrames = currentClearTTY.getFrames(); + + t.deepEqual(frames, currentFrames); + // Frames created using new clear method are deep equal to frames created using current clear method +}); + +test('new clear method test, erases wrapped lines', t => { + const transformTTY = new TransformTTY({crlf: true, columns: 40}); + transformTTY.addSequencer(); + transformTTY.addSequencer(null, true); + + const currentClearTTY = new TransformTTY({crlf: true, columns: 40}); + currentClearTTY.addSequencer(); + + const currentOra = currentClearMethod(currentClearTTY); + + const cursorAtRow = () => { + const cursor = transformTTY.getCursorPos(); + return cursor.y === 0 ? 0 : cursor.y * -1; + }; + + const clearedLines = () => { + return transformTTY.toString().split('\n').length; + }; + + const spinner = new Ora({ + text: 'foo', + color: false, + isEnabled: true, + stream: transformTTY, + spinner: { + frames: ['-'] + } + }); + + currentOra.render(); + + spinner.render(); + t.is(clearedLines(), 1); // Cleared 'foo' + t.is(cursorAtRow(), 0); + + currentOra.text = 'foo\n\nbar'; + currentOra.render(); + + spinner.text = 'foo\n\nbar'; + spinner.render(); + t.is(clearedLines(), 3); // Cleared 'foo\n\nbar' + t.is(cursorAtRow(), -2); + + currentOra.clear(); + currentOra.text = '0'.repeat(currentOra.stream.columns + 10); + currentOra.render(); + currentOra.render(); + + spinner.clear(); + spinner.text = '0'.repeat(spinner.stream.columns + 10); + spinner.render(); + spinner.render(); + t.is(clearedLines(), 2); + t.is(cursorAtRow(), -1); + + currentOra.clear(); + currentOra.text = '๐Ÿฆ„'.repeat(currentOra.stream.columns + 10); + currentOra.render(); + currentOra.render(); + + spinner.clear(); + spinner.text = '๐Ÿฆ„'.repeat(spinner.stream.columns + 10); + spinner.render(); + spinner.render(); + t.is(clearedLines(), 3); + t.is(cursorAtRow(), -2); + + currentOra.clear(); + currentOra.text = '๐Ÿฆ„'.repeat(currentOra.stream.columns - 2) + '\nfoo'; + currentOra.render(); + currentOra.render(); + + spinner.clear(); + spinner.text = '๐Ÿฆ„'.repeat(spinner.stream.columns - 2) + '\nfoo'; + spinner.render(); + spinner.render(); + t.is(clearedLines(), 3); + t.is(cursorAtRow(), -2); + + currentOra.clear(); + currentOra.prefixText = 'foo\n'; + currentOra.text = '\nbar'; + currentOra.render(); + currentOra.render(); + + spinner.clear(); + spinner.prefixText = 'foo\n'; + spinner.text = '\nbar'; + spinner.render(); + spinner.render(); + t.is(clearedLines(), 3); // Cleared 'foo\n\nbar' + t.is(cursorAtRow(), -2); + + const [sequenceString, clearedSequenceString] = transformTTY.getSequenceStrings(); + const [frames, clearedFrames] = transformTTY.getFrames(); + + t.is(sequenceString, 'foo\n - \nbar'); + t.is(sequenceString, clearedSequenceString); + + t.deepEqual(clearedFrames, [ + '- foo', + '- foo\n\nbar', + '- 00000000000000000000000000000000000000\n000000000000', + '- 00000000000000000000000000000000000000\n000000000000', + '- ๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + '๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + '๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„', + '- ๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + '๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + '๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„', + '- ๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + '๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + 'foo', + '- ๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + '๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„๐Ÿฆ„\n' + + 'foo', + 'foo\n - \nbar', + 'foo\n - \nbar' + ]); + + t.deepEqual(frames, clearedFrames); + + const currentClearString = currentClearTTY.toString(); + t.is(currentClearString, 'foo\n - \nbar'); + + const currentFrames = currentClearTTY.getFrames(); + t.deepEqual(frames, currentFrames); +}); + +test('new clear method, stress test', t => { + const rando = (min, max) => { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * ((max - min) + min)); + }; + + const rAnDoMaNiMaLs = (min, max) => { + const length = rando(min, max); + let result = ''; + const THEAMINALS = ['๐Ÿฏ', '๐Ÿฆ', '๐Ÿฎ', '๐Ÿท', '๐Ÿฝ', '๐Ÿธ', '๐Ÿ™', '๐Ÿต', '๐Ÿฆ', '๐Ÿง', '๐Ÿ”', '๐Ÿ’', '๐Ÿ™‰', '๐Ÿ™ˆ', '๐Ÿฃ', '๐Ÿฅ', '๐Ÿบ', '๐Ÿ—', '๐Ÿด', '๐Ÿฆ„', '๐Ÿ', '๐Ÿ›', ...Array.from({length: 5}).fill('\n')]; + + for (let i = 0; i < length; i++) { + result += THEAMINALS[Math.floor(Math.random() * THEAMINALS.length)]; + } + + return result; + }; + + const randos = () => { + return rAnDoMaNiMaLs(rando(5, 15), rando(25, 50)); + }; + + const randomize = (s1, s2) => { + const spnr = cliSpinners.random; + const txt = randos(); + const indent = rando(0, 15); + + s1.spinner = spnr; + s2.spinner = spnr; + s1.text = txt; + s2.text = txt; + s1.indent = indent; + s2.indent = indent; + }; + + const transformTTY = new TransformTTY({crlf: true}); + transformTTY.addSequencer(); + transformTTY.addSequencer(null, true); + + const currentClearTTY = new TransformTTY({crlf: true}); + currentClearTTY.addSequencer(); + + const currentOra = currentClearMethod(currentClearTTY); + + const spinner = new Ora({ + color: false, + isEnabled: true, + stream: transformTTY + }); + + randomize(spinner, currentOra); + + for (let x = 0; x < 100; x++) { + if (x % 10 === 0) { + randomize(spinner, currentOra); + } + + if (x % 5 === 0) { + const indent = rando(0, 25); + spinner.indent = indent; + currentOra.indent = indent; + } + + if (x % 15 === 0) { + let {text} = spinner; + const loops = rando(1, 10); + + for (let x = 0; x < loops; x++) { + const pos = Math.floor(Math.random() * text.length); + text = text.slice(0, pos) + '\n' + text.slice(pos + 1); + } + + spinner.text = text; + currentOra.text = text; + } + + spinner.render(); + currentOra.render(); + } + + spinner.succeed('๐Ÿ™‰'); + currentOra.succeed('๐Ÿ™‰'); + + const currentFrames = currentClearTTY.getFrames(); + const [frames, clearedFrames] = transformTTY.getFrames(); + + t.deepEqual(frames, clearedFrames); + + t.deepEqual(frames.slice(0, currentFrames.length), currentFrames); + + // Console.log(frames); + // console.log(clearFrames); +}); +/* +Example output: + +[ + ' โ– \n', + ' โ–Ž \n', + ' โ– \n', + ' โ–Œ \n', + ' โ–‹ \n', + ' โ–Š \n', + ' โ–‰ \n', + ' โ–Š \n', + ' โ–‹ \n', + ' โ–Œ \n', + ' d ', + ' q ', + ' p ', + ' b ', + ' d ', + ' q \n', + ' p \n', + ' b \n', + ' d \n', + ' q \n', + ' โ—ข ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฃ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ค ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฅ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ข ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฃ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ค ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฅ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ข ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฃ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ ‹ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ™ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ น \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ธ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ผ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ด \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ฆ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ง \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ‡ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ  \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ–ก ', + ' โ–  ', + ' โ–ก ', + ' โ–  ', + ' โ–ก ', + ' โ–  \n', + ' โ–ก \n', + ' โ–  \n', + ' โ–ก \n', + ' โ–  \n', + ' . ๐Ÿ—', + ' .. ๐Ÿ—', + ' ... ๐Ÿ—', + ' ๐Ÿ—', + ' . ๐Ÿ—', + ' .. ๐Ÿ—', + ' ... ๐Ÿ—', + ' ๐Ÿ—', + ' . ๐Ÿ—', + ' .. ๐Ÿ—', + ' โ–– ๐Ÿ”\n๐Ÿธ\n', + ' โ–˜ ๐Ÿ”\n๐Ÿธ\n', + ' โ– ๐Ÿ”\n๐Ÿธ\n', + ' โ–— ๐Ÿ”\n๐Ÿธ\n', + ' โ–– ๐Ÿ”\n๐Ÿธ\n', + ' โ–˜ ๐Ÿ”\n๐Ÿธ\n', + ' โ– ๐Ÿ”\n๐Ÿธ\n', + ' โ–— ๐Ÿ”\n๐Ÿธ\n', + ' โ–– ๐Ÿ”\n๐Ÿธ\n', + ' โ–˜ ๐Ÿ”\n๐Ÿธ\n', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ—) ๐Ÿ”๐Ÿ—', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '(โ— ) ๏ฟฝ\n\n๏ฟฝ', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' - ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' ` ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' ` ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + " ' ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n", + ' ยด ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' - ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ... 1 more item +] +[ + ' โ– \n', + ' โ–Ž \n', + ' โ– \n', + ' โ–Œ \n', + ' โ–‹ \n', + ' โ–Š \n', + ' โ–‰ \n', + ' โ–Š \n', + ' โ–‹ \n', + ' โ–Œ \n', + ' d ', + ' q ', + ' p ', + ' b ', + ' d ', + ' q \n', + ' p \n', + ' b \n', + ' d \n', + ' q \n', + ' โ—ข ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฃ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ค ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฅ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ข ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฃ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ค ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฅ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ข ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ—ฃ ๐Ÿ—๐Ÿง๐Ÿฅ๐Ÿบ๐Ÿต\n\n', + ' โ ‹ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ™ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ น \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ธ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ผ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ด \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ฆ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ง \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ ‡ \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ  \n๏ฟฝ๐Ÿฎ๏ฟฝ\n\n๏ฟฝ\n', + ' โ–ก ', + ' โ–  ', + ' โ–ก ', + ' โ–  ', + ' โ–ก ', + ' โ–  \n', + ' โ–ก \n', + ' โ–  \n', + ' โ–ก \n', + ' โ–  \n', + ' . ๐Ÿ—', + ' .. ๐Ÿ—', + ' ... ๐Ÿ—', + ' ๐Ÿ—', + ' . ๐Ÿ—', + ' .. ๐Ÿ—', + ' ... ๐Ÿ—', + ' ๐Ÿ—', + ' . ๐Ÿ—', + ' .. ๐Ÿ—', + ' โ–– ๐Ÿ”\n๐Ÿธ\n', + ' โ–˜ ๐Ÿ”\n๐Ÿธ\n', + ' โ– ๐Ÿ”\n๐Ÿธ\n', + ' โ–— ๐Ÿ”\n๐Ÿธ\n', + ' โ–– ๐Ÿ”\n๐Ÿธ\n', + ' โ–˜ ๐Ÿ”\n๐Ÿธ\n', + ' โ– ๐Ÿ”\n๐Ÿธ\n', + ' โ–— ๐Ÿ”\n๐Ÿธ\n', + ' โ–– ๐Ÿ”\n๐Ÿธ\n', + ' โ–˜ ๐Ÿ”\n๐Ÿธ\n', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ— ) ๐Ÿ”๐Ÿ—', + ' ( โ—) ๐Ÿ”๐Ÿ—', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '( โ— ) ๏ฟฝ\n\n๏ฟฝ', + '(โ— ) ๏ฟฝ\n\n๏ฟฝ', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง‡ ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' โง† ๐Ÿท๐Ÿ›๐Ÿ”๐Ÿฆ๐Ÿท๐Ÿ™‰', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' - ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' ` ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' ` ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + " ' ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n", + ' ยด ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' - ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ' _ ๐Ÿฝ๐Ÿฆ„๐Ÿฃ\n๐Ÿฃ๐Ÿง๐Ÿ”๐Ÿฆ๐Ÿฆ๏ฟฝ\n', + ... 1 more item +] +*/