« 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
path: root/util/interfaces/Flushable.js
diff options
context:
space:
mode:
Diffstat (limited to 'util/interfaces/Flushable.js')
-rw-r--r--util/interfaces/Flushable.js126
1 files changed, 126 insertions, 0 deletions
diff --git a/util/interfaces/Flushable.js b/util/interfaces/Flushable.js
new file mode 100644
index 0000000..d8b72d3
--- /dev/null
+++ b/util/interfaces/Flushable.js
@@ -0,0 +1,126 @@
+import * as ansi from '../ansi.js'
+import unic from '../unichars.js'
+
+export default class Flushable {
+  // A writable that can be used to collect chunks of data before writing
+  // them.
+
+  constructor(writable, shouldCompress = false) {
+    this.target = writable
+
+    // Use the magical ANSI self-made compression method that probably
+    // doesn't *quite* work but should drastically decrease write size?
+    this.shouldCompress = shouldCompress
+
+    // Whether or not to show compression statistics (original written size
+    // and ANSI-interpreted compressed size) in the output of flush.
+    this.shouldShowCompressionStatistics = false
+
+    // Use resizeScreen if you plan on using the ANSI compressor!
+    this.screenLines = 24
+    this.screenCols = 80
+    this.lastFrame = undefined
+
+    this.ended = false
+    this.paused = false
+    this.requestedFlush = false
+
+    this.chunks = []
+  }
+
+  resizeScreen({lines, cols}) {
+    this.screenLines = lines
+    this.screenCols = cols
+    this.clearLastFrame()
+  }
+
+  clearLastFrame() {
+    this.lastFrame = undefined
+  }
+
+  write(what) {
+    this.chunks.push(what)
+  }
+
+  flush() {
+    // If we're paused, we don't want to write, but we will keep a note that a
+    // flush was requested for when we unpause.
+    if (this.paused) {
+      this.requestedFlush = true
+      return
+    }
+
+    // Don't write if we've ended.
+    if (this.ended) {
+      return
+    }
+
+    // End if the target is destroyed.
+    // Yes, this relies on the target having a destroyed property
+    // Don't worry, it'll still work if there is no destroyed property though
+    // (I think)
+    if (this.target.destroyed) {
+      this.end()
+      return
+    }
+
+    let toWrite = this.chunks.join('')
+
+    if (this.shouldCompress) {
+      toWrite = this.compress(toWrite)
+    }
+
+    try {
+      this.target.write(toWrite)
+    } catch(err) {
+      console.error('Flushable write error (ending):', err.message)
+      this.end()
+    }
+
+    this.chunks = []
+  }
+
+  pause() {
+    this.paused = true
+  }
+
+  resume() {
+    this.paused = false
+
+    if (this.requestedFlush) {
+      this.flush()
+    }
+  }
+
+  end() {
+    this.ended = true
+  }
+
+  compress(toWrite) {
+    // TODO: customize screen size
+    const output = ansi.interpret(
+      toWrite, this.screenLines, this.screenCols, this.lastFrame
+    )
+
+    let { screen } = output
+
+    this.lastFrame = output
+
+    if (this.shouldShowCompressionStatistics) {
+      let msg = this.lastInterpretMessage
+      if (screen.length > 0 || !this.lastInterpretMessage) {
+        const pcSaved = Math.round(1000 - (1000 / toWrite.length * screen.length)) / 10
+        const kbSaved = Math.round((toWrite.length - screen.length) / 100) / 10
+        msg = this.lastInterpretMessage = (
+          '(ANSI-interpret: ' +
+          `${toWrite.length} -> ${screen.length} ${pcSaved}% / ${kbSaved} KB saved)`
+        )
+      }
+      screen += '\x1b[H\x1b[0m'
+      screen += msg + unic.BOX_H_DOUBLE.repeat(this.screenCols - msg.length)
+      this.lastFrame.oldLastChar.attributes = []
+    }
+
+    return screen
+  }
+}