« get me outta code hell

tui-lib - Pure Node.js library for making visual command-line programs (ala vim, ncdu)
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--util/Flushable.js13
-rw-r--r--util/ansi.js145
2 files changed, 116 insertions, 42 deletions
diff --git a/util/Flushable.js b/util/Flushable.js
index 2248fec..78bf5c5 100644
--- a/util/Flushable.js
+++ b/util/Flushable.js
@@ -86,14 +86,21 @@ module.exports = class Flushable {
 
   compress(toWrite) {
     // TODO: customize screen size
-    let screen = ansi.interpret(toWrite, this.screenLines, this.screenCols)
+    let { newChars, lastChar, screen } = ansi.interpret(
+      toWrite, this.screenLines, this.screenCols,
+      this.lastFrameChars, this.lastFrameLastChar
+    )
+
+    this.lastFrameChars = newChars
+    this.lastFrameLastChar = lastChar
 
     if (this.shouldShowCompressionStatistics) {
       const pcSaved = Math.round(100 - (100 / toWrite.length * screen.length))
       screen += (
-        '\x1b[1A (ANSI-interpret: ' +
-        `${toWrite.length} -> ${screen.length} ${pcSaved}% saved)`
+        '\x1b[H\x1b[0m(ANSI-interpret: ' +
+        `${toWrite.length} -> ${screen.length} ${pcSaved}% saved)  `
       )
+      this.lastFrameLastChar.attributes = []
     }
 
     return screen
diff --git a/util/ansi.js b/util/ansi.js
index c796a2e..cfe7b1f 100644
--- a/util/ansi.js
+++ b/util/ansi.js
@@ -119,7 +119,7 @@ const ansi = {
   },
 
 
-  interpret(text, scrRows, scrCols) {
+  interpret(text, scrRows, scrCols, oldChars = null, lastChar = null) {
     // Interprets the given ansi code, more or less.
 
     const blank = {
@@ -285,56 +285,123 @@ const ansi = {
       }
     }
 
-    // Character concatenation -----------
+    // SPOooooOOoky diffing! -------------
+    //
+    // - Search for series of differences. This means a collection of characters
+    //   which have different text or attribute properties.
+    //
+    // - Figure out how to print these differences. Move the cursor to the beginning
+    //   character's row/column, then print the differences.
+
+    const newChars = chars
+
+    const differences = []
+
+    if (oldChars === null) {
+      differences.push({i: 0, chars: [...newChars]})
+    } else {
+      const charsEqual = (oldChar, newChar) => {
+        // TODO: Check attributes.
+
+        if (oldChar.char !== newChar.char) {
+          return false
+        }
+
+        let oldAttrs = oldChar.attributes.slice()
+        let newAttrs = newChar.attributes.slice()
+
+        while (newAttrs.length) {
+          const attr = newAttrs.shift()
+          if (oldAttrs.includes(attr)) {
+            oldAttrs.splice(oldAttrs.indexOf(attr), 1)
+          } else {
+            return false
+          }
+        }
+
+        oldAttrs = oldChar.attributes.slice()
+        newAttrs = newChar.attributes.slice()
+
+        while (oldAttrs.length) {
+          const attr = oldAttrs.shift()
+          if (newAttrs.includes(attr)) {
+            newAttrs.splice(newAttrs.indexOf(attr), 1)
+          } else {
+            return false
+          }
+        }
+
+        return true
+      }
+
+      let curDiff = null
+
+      for (let i = 0; i < chars.length; i++) {
+        const oldChar = oldChars[i]
+        const newChar = newChars[i]
+
+        // TODO: Some sort of "distance" before we should clear curDiff?
+        // It may take *less* characters if this diff and the next are merged
+        // (entering a single character is smaller than the length of the code
+        // used to move past that character). Probably not very significant of
+        // an impact, though.
+        if (charsEqual(oldChar, newChar)) {
+          curDiff = null
+        } else {
+          if (curDiff === null) {
+            curDiff = {i, chars: []}
+            differences.push(curDiff)
+          }
+
+          curDiff.chars.push(newChar)
+        }
+      }
+    }
 
-    // Move to the top left of the screen initially.
-    const result = [ ansi.moveCursorRaw(1, 1) ]
+    // Character concatenation -----------
 
-    let lastChar = {
+    lastChar = lastChar || {
       char: '',
       attributes: []
     }
 
-    //let n = 1 // debug
-
-    for (const char of chars) {
-      const newAttributes = (
-        char.attributes.filter(attr => !(lastChar.attributes.includes(attr)))
-      )
-
-      const removedAttributes = (
-        lastChar.attributes.filter(attr => !(char.attributes.includes(attr)))
-      )
-
-      // The only way to practically remove any character attribute is to
-      // reset all of its attributes and then re-add its existing attributes.
-      // If we do that, there's no need to add new attributes.
-      if (removedAttributes.length) {
-        // console.log(
-        //   `removed some attributes "${char.char}"`, removedAttributes
-        // )
-        result.push(ansi.resetAttributes())
-        result.push(`${ESC}[${char.attributes.join(';')}m`)
-      } else if (newAttributes.length) {
-        result.push(`${ESC}[${newAttributes.join(';')}m`)
-      }
+    const result = []
 
-      // debug
-      /*
-      if (char.char !== ' ') {
-        console.log(
-          `#2-char ${char.char}; ${chars.indexOf(char) - n} inbetween`
+    for (const diff of differences) {
+      const col = diff.i % scrCols
+      const row = (diff.i - col) / scrCols
+      result.push(ansi.moveCursor(row, col))
+
+      for (const char of diff.chars) {
+        const newAttributes = (
+          char.attributes.filter(attr => !(lastChar.attributes.includes(attr)))
         )
-        n = chars.indexOf(char)
-      }
-      */
 
-      result.push(char.char)
+        const removedAttributes = (
+          lastChar.attributes.filter(attr => !(char.attributes.includes(attr)))
+        )
 
-      lastChar = char
+        // The only way to practically remove any character attribute is to
+        // reset all of its attributes and then re-add its existing attributes.
+        // If we do that, there's no need to add new attributes.
+        if (removedAttributes.length) {
+          result.push(ansi.resetAttributes())
+          result.push(`${ESC}[${char.attributes.join(';')}m`)
+        } else if (newAttributes.length) {
+          result.push(`${ESC}[${newAttributes.join(';')}m`)
+        }
+
+        result.push(char.char)
+
+        lastChar = char
+      }
     }
 
-    return result.join('')
+    return {
+      newChars: newChars.slice(),
+      lastChar: Object.assign({}, lastChar),
+      screen: result.join('')
+    }
   }
 }