« get me outta code hell

A long-due cleanup + examples + things - 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:
authorliam4 <towerofnix@gmail.com>2017-07-03 18:59:57 -0300
committerliam4 <towerofnix@gmail.com>2017-07-03 19:00:01 -0300
commit769413468e88acba1a180baa0113139d929a3b9f (patch)
treef29af36826077178259b7bcc8bf9927cebfe71e3
parent489e4d0c78d5f393729cda0e1f6ac9a0a1237b4a (diff)
A long-due cleanup + examples + things
..Obviously this breaks old things (particularly, see changes in
FocusElement).
-rw-r--r--examples/basic-app.js51
-rw-r--r--examples/interfacer-command-line.js24
-rw-r--r--examples/interfacer-telnet.js56
-rw-r--r--examples/label.js22
-rw-r--r--ui/DisplayElement.js2
-rw-r--r--ui/Label.js2
-rw-r--r--ui/Pane.js4
-rw-r--r--ui/Root.js104
-rw-r--r--ui/Sprite.js2
-rw-r--r--ui/form/Button.js6
-rw-r--r--ui/form/CancelDialog.js4
-rw-r--r--ui/form/ConfirmDialog.js4
-rw-r--r--ui/form/FocusBox.js6
-rw-r--r--ui/form/FocusElement.js12
-rw-r--r--ui/form/Form.js4
-rw-r--r--ui/form/HorizontalForm.js4
-rw-r--r--ui/form/TextInput.js6
-rw-r--r--util/CommandLineInterfacer.js86
-rw-r--r--util/Flushable.js (renamed from Flushable.js)21
-rw-r--r--util/TelnetInterfacer.js137
-rw-r--r--util/ansi.js (renamed from ansi.js)15
-rw-r--r--util/exception.js (renamed from exception.js)0
-rw-r--r--util/telchars.js (renamed from telchars.js)0
-rw-r--r--util/unichars.js (renamed from unichars.js)0
-rw-r--r--util/waitForData.js9
25 files changed, 455 insertions, 126 deletions
diff --git a/examples/basic-app.js b/examples/basic-app.js
new file mode 100644
index 0000000..bf8aa41
--- /dev/null
+++ b/examples/basic-app.js
@@ -0,0 +1,51 @@
+// Basic app demo:
+// - Structuring a basic element tree
+// - Creating a pane and text input
+// - Using content width/height to layout elements
+// - Subclassing a FocusElement and using its focused method
+// - Sending a quit-app request via Control-C
+//
+// This script cannot actually be used on its own; see the examples on
+// interfacers (interfacer-command-line.js and inerfacer-telnet.js) for a
+// working demo.
+
+const Pane = require('../ui/Pane')
+const FocusElement = require('../ui/form/FocusElement')
+const TextInput = require('../ui/form/TextInput')
+
+module.exports = class AppElement extends FocusElement {
+  constructor() {
+    super()
+
+    this.pane = new Pane()
+    this.addChild(this.pane)
+
+    this.textInput = new TextInput()
+    this.pane.addChild(this.textInput)
+  }
+
+  fixLayout() {
+    this.w = this.parent.contentW
+    this.h = this.parent.contentH
+
+    this.pane.w = this.contentW
+    this.pane.h = this.contentH
+
+    this.textInput.x = 4
+    this.textInput.y = 2
+    this.textInput.w = this.pane.contentW - 8
+  }
+
+  focused() {
+    this.root.select(this.textInput)
+  }
+
+  keyPressed(keyBuf) {
+    if (keyBuf[0] === 0x03) { // 0x03 is Control-C
+      this.emit('quitRequested')
+      return 
+    }
+
+    super.keyPressed(keyBuf)
+  }
+}
diff --git a/examples/interfacer-command-line.js b/examples/interfacer-command-line.js
new file mode 100644
index 0000000..1da6adf
--- /dev/null
+++ b/examples/interfacer-command-line.js
@@ -0,0 +1,24 @@
+const Root = require('../ui/Root')
+const CommandLineInterfacer = require('../util/CommandLineInterfacer')
+const AppElement = require('./basic-app')
+
+const interfacer = new CommandLineInterfacer()
+
+interfacer.getScreenSize().then(size => {
+  const root = new Root(interfacer)
+  root.w = size.width
+  root.h = size.height
+
+  const appElement = new AppElement()
+  root.addChild(appElement)
+  root.select(appElement)
+
+  appElement.on('quitRequested', () => {
+    process.exit(0)
+  })
+
+  setInterval(() => root.render(), 100)
+}).catch(error => {
+  console.error(error)
+  process.exit(1)
+})
diff --git a/examples/interfacer-telnet.js b/examples/interfacer-telnet.js
new file mode 100644
index 0000000..8cb804b
--- /dev/null
+++ b/examples/interfacer-telnet.js
@@ -0,0 +1,56 @@
+// Telnet demo:
+// - Basic telnet socket handling using the TelnetInterfacer
+// - Handling client's screen size
+// - Handling socket being closed by client
+// - Handling cleanly closing the socket by hand
+
+const net = require('net')
+const Root = require('../ui/Root')
+const TelnetInterfacer = require('../TelnetInterfacer')
+const AppElement = require('./basic-app')
+
+const server = new net.Server(socket => {
+  const interfacer = new TelnetInterfacer(socket)
+
+  interfacer.getScreenSize().then(size => {
+    const root = new Root(interfacer)
+    root.w = size.width
+    root.h = size.height
+
+    interfacer.on('screenSizeUpdated', newSize => {
+      root.w = newSize.width
+      root.h = newSize.height
+      root.fixAllLayout()
+    })
+
+    const appElement = new AppElement()
+    root.addChild(appElement)
+    root.select(appElement)
+
+    let closed = false
+
+    appElement.on('quitRequested', () => {
+      if (!closed) {
+        interfacer.cleanTelnetOptions()
+        socket.write('Goodbye!\n')
+        socket.end()
+        clearInterval(interval)
+        closed = true
+      }
+    })
+
+    socket.on('close', () => {
+      if (!closed) {
+        clearInterval(interval)
+        closed = true
+      }
+    })
+
+    const interval = setInterval(() => root.render(), 100)
+  }).catch(error => {
+    console.error(error)
+    process.exit(1)
+  })
+})
+
+server.listen(8008)
diff --git a/examples/label.js b/examples/label.js
new file mode 100644
index 0000000..b8992d2
--- /dev/null
+++ b/examples/label.js
@@ -0,0 +1,22 @@
+// An example of basic label usage.
+
+const ansi = require('../util/ansi')
+const Label = require('../ui/Label')
+
+const label1 = new Label('Hello, world!')
+const label2 = new Label('I love labels.')
+
+label1.x = 3
+label1.y = 2
+
+label2.x = label1.x
+label2.y = label1.y + 1
+
+process.stdout.write(ansi.clearScreen())
+label1.drawTo(process.stdout)
+label2.drawTo(process.stdout)
+
+process.stdin.once('data', () => {
+  process.stdout.write(ansi.clearScreen())
+  process.exit(0)
+})
diff --git a/ui/DisplayElement.js b/ui/DisplayElement.js
index c8352ed..3a97ed9 100644
--- a/ui/DisplayElement.js
+++ b/ui/DisplayElement.js
@@ -1,5 +1,5 @@
 const EventEmitter = require('events')
-const exception = require('../exception')
+const exception = require('../util/exception')
 
 module.exports = class DisplayElement extends EventEmitter {
   // A general class that handles dealing with screen coordinates, the tree
diff --git a/ui/Label.js b/ui/Label.js
index 60ece15..850edc0 100644
--- a/ui/Label.js
+++ b/ui/Label.js
@@ -1,4 +1,4 @@
-const ansi = require('../ansi')
+const ansi = require('../util/ansi')
 
 const DisplayElement = require('./DisplayElement')
 
diff --git a/ui/Pane.js b/ui/Pane.js
index b4fad57..4e08c55 100644
--- a/ui/Pane.js
+++ b/ui/Pane.js
@@ -1,5 +1,5 @@
-const ansi = require('../ansi')
-const unic = require('../unichars')
+const ansi = require('../util/ansi')
+const unic = require('../util/unichars')
 
 const DisplayElement = require('./DisplayElement')
 
diff --git a/ui/Root.js b/ui/Root.js
index 06e3ecd..b170f99 100644
--- a/ui/Root.js
+++ b/ui/Root.js
@@ -1,6 +1,6 @@
 const iac = require('iac')
 
-const ansi = require('../ansi')
+const ansi = require('../util/ansi')
 
 const DisplayElement = require('./DisplayElement')
 
@@ -10,88 +10,23 @@ module.exports = class Root extends DisplayElement {
   // An element to be used as the root of a UI. Handles lots of UI and
   // socket stuff.
 
-  constructor(socket) {
+  constructor(interfacer) {
     super()
 
-    this.socket = socket
-    this.initTelnetOptions()
+    this.interfacer = interfacer
 
     this.selected = null
 
     this.cursorBlinkOffset = Date.now()
 
-    socket.on('data', buf => this.handleData(buf))
+    interfacer.on('inputData', buf => this.handleData(buf))
   }
 
-  initTelnetOptions() {
-    // Initializes various socket options, using telnet magic.
-
-    // Disables linemode.
-    this.socket.write(Buffer.from([
-      255, 253, 34,  // IAC DO LINEMODE
-      255, 250, 34, 1, 0, 255, 240,  // IAC SB LINEMODE MODE 0 IAC SE
-      255, 251, 1    // IAC WILL ECHO
-    ]))
-
-    // Will SGA. Helps with putty apparently.
-    this.socket.write(Buffer.from([
-      255, 251, 3  // IAC WILL SGA
-    ]))
-
-    this.socket.write(ansi.hideCursor())
-  }
-
-  cleanTelnetOptions() {
-    // Resets the telnet options and magic set in initTelnetOptions.
-
-    this.socket.write(ansi.resetAttributes())
-    this.socket.write(ansi.showCursor())
-  }
-
-  requestTelnetWindowSize() {
-    // See RFC #1073 - Telnet Window Size Option
-
-    return new Promise((res, rej) => {
-      this.socket.write(Buffer.from([
-        255, 253, 31  // IAC WILL NAWS
-      ]))
-
-      this.once('telnetsub', function until(sub) {
-        if (sub[0] !== 31) { // NAWS
-          this.once('telnetsub', until)
-        } else {
-          res({lines: sub[4], cols: sub[2]})
-        }
-      })
-    })
+  render() {
+    this.renderTo(this.interfacer)
   }
 
   handleData(buffer) {
-    if (buffer[0] === 255) {
-      // Telnet IAC (Is A Command) - ignore
-
-      // Split the data into multiple IAC commands if more than one IAC was
-      // sent.
-      const values = Array.from(buffer.values())
-      const commands = []
-      const curCmd = [255]
-      for (let value of values) {
-        if (value === 255) { // IAC
-          commands.push(Array.from(curCmd))
-          curCmd.splice(1, curCmd.length)
-          continue
-        }
-        curCmd.push(value)
-      }
-      commands.push(curCmd)
-
-      for (let command of commands) {
-        this.interpretTelnetCommand(command)
-      }
-
-      return
-    }
-
     if (this.selected) {
       const els = this.selected.directAncestors.concat([this.selected])
       for (let el of els) {
@@ -106,29 +41,6 @@ module.exports = class Root extends DisplayElement {
     }
   }
 
-  interpretTelnetCommand(command) {
-    if (command[0] !== 255) { // IAC
-      // First byte isn't IAC, which means this isn't a command, so do
-      // nothing.
-      return
-    }
-
-    if (command[1] === 251) { // WILL
-      // Do nothing because I'm lazy
-      const willWhat = command[2]
-      //console.log('IAC WILL ' + willWhat)
-    }
-
-    if (command[1] === 250) { // SB
-      this.telnetSub = command.slice(2)
-    }
-
-    if (command[1] === 240) { // SE
-      this.emit('telnetsub', this.telnetSub)
-      this.telnetSub = null
-    }
-  }
-
   drawTo(writable) {
     writable.write(ansi.moveCursor(0, 0))
     writable.write(' '.repeat(this.w * this.h))
@@ -164,11 +76,11 @@ module.exports = class Root extends DisplayElement {
     // element, if there is one.
 
     if (this.selected) {
-      this.selected.unfocus()
+      this.selected.unfocused()
     }
 
     this.selected = el
-    this.selected.focus()
+    this.selected.focused()
 
     this.cursorMoved()
   }
diff --git a/ui/Sprite.js b/ui/Sprite.js
index cd6528c..62b0172 100644
--- a/ui/Sprite.js
+++ b/ui/Sprite.js
@@ -1,4 +1,4 @@
-const ansi = require('../ansi')
+const ansi = require('../util/ansi')
 
 const DisplayElement = require('./DisplayElement')
 
diff --git a/ui/form/Button.js b/ui/form/Button.js
index 9a3d2f7..86347a0 100644
--- a/ui/form/Button.js
+++ b/ui/form/Button.js
@@ -1,5 +1,5 @@
-const ansi = require('../../ansi')
-const telc = require('../../telchars')
+const ansi = require('../../util/ansi')
+const telc = require('../../util/telchars')
 
 const FocusElement = require('./FocusElement')
 
@@ -29,7 +29,7 @@ module.exports = class ButtonInput extends FocusElement {
   }
 
   drawTo(writable) {
-    if (this.isSelected) {
+    if (this.isFocused) {
       writable.write(ansi.invert())
     }
 
diff --git a/ui/form/CancelDialog.js b/ui/form/CancelDialog.js
index ba9faf8..c5eb7d3 100644
--- a/ui/form/CancelDialog.js
+++ b/ui/form/CancelDialog.js
@@ -1,4 +1,4 @@
-const telc = require('../../telchars')
+const telc = require('../../util/telchars')
 
 const FocusElement = require('./FocusElement')
 
@@ -47,7 +47,7 @@ module.exports = class ConfirmDialog extends FocusElement {
     this.cancelBtn.y = this.pane.contentH - 2
   }
 
-  focus() {
+  focused() {
     this.root.select(this.cancelBtn)
   }
 
diff --git a/ui/form/ConfirmDialog.js b/ui/form/ConfirmDialog.js
index 614dede..3614cf9 100644
--- a/ui/form/ConfirmDialog.js
+++ b/ui/form/ConfirmDialog.js
@@ -1,4 +1,4 @@
-const telc = require('../../telchars')
+const telc = require('../../util/telchars')
 
 const FocusElement = require('./FocusElement')
 
@@ -59,7 +59,7 @@ module.exports = class ConfirmDialog extends FocusElement {
     this.cancelBtn.y = this.form.contentH - 2
   }
 
-  focus() {
+  focused() {
     this.root.select(this.form)
   }
 
diff --git a/ui/form/FocusBox.js b/ui/form/FocusBox.js
index c259f23..51e961b 100644
--- a/ui/form/FocusBox.js
+++ b/ui/form/FocusBox.js
@@ -1,4 +1,4 @@
-const ansi = require('../../ansi')
+const ansi = require('../../util/ansi')
 
 const FocusElement = require('./FocusElement')
 
@@ -19,13 +19,13 @@ module.exports = class FocusBox extends FocusElement {
   }
 
   drawTo(writable) {
-    if (this.isSelected) {
+    if (this.isFocused) {
       writable.write(ansi.invert())
     }
   }
 
   didRenderTo(writable) {
-    if (this.isSelected) {
+    if (this.isFocused) {
       writable.write(ansi.resetAttributes())
     }
   }
diff --git a/ui/form/FocusElement.js b/ui/form/FocusElement.js
index 25a0693..5967e26 100644
--- a/ui/form/FocusElement.js
+++ b/ui/form/FocusElement.js
@@ -9,19 +9,19 @@ module.exports = class FocusElement extends DisplayElement {
     this.cursorX = 0
     this.cursorY = 0
 
-    this.isSelected = false
+    this.isFocused = false
   }
 
-  focus(socket) {
-    // Do something with socket. Should be overridden in subclasses.
+  focused() {
+    // Should be overridden in subclasses.
 
-    this.isSelected = true
+    this.isFocused = true
   }
 
-  unfocus() {
+  unfocused() {
     // Should be overridden in subclasses.
 
-    this.isSelected = false
+    this.isFocused = false
   }
 
   keyPressed(keyBuf) {
diff --git a/ui/form/Form.js b/ui/form/Form.js
index 49fa075..9274da4 100644
--- a/ui/form/Form.js
+++ b/ui/form/Form.js
@@ -1,4 +1,4 @@
-const telc = require('../../telchars')
+const telc = require('../../util/telchars')
 
 const FocusElement = require('./FocusElement')
 
@@ -45,7 +45,7 @@ module.exports = class Form extends FocusElement {
     }
   }
   
-  focus() {
+  focused() {
     this.root.select(this.inputs[this.curIndex])
   }
 }
diff --git a/ui/form/HorizontalForm.js b/ui/form/HorizontalForm.js
deleted file mode 100644
index 141bb17..0000000
--- a/ui/form/HorizontalForm.js
+++ /dev/null
@@ -1,4 +0,0 @@
-const Form = require('./DisplayElement')
-
-module.exports = class HorizontalBox extends Box {
-}
diff --git a/ui/form/TextInput.js b/ui/form/TextInput.js
index d09480f..fc59cbb 100644
--- a/ui/form/TextInput.js
+++ b/ui/form/TextInput.js
@@ -1,6 +1,6 @@
-const ansi = require('../../ansi')
-const unic = require('../../unichars')
-const telc = require('../../telchars')
+const ansi = require('../../util/ansi')
+const unic = require('../../util/unichars')
+const telc = require('../../util/telchars')
 
 const FocusElement = require('./FocusElement')
 
diff --git a/util/CommandLineInterfacer.js b/util/CommandLineInterfacer.js
new file mode 100644
index 0000000..3f9d208
--- /dev/null
+++ b/util/CommandLineInterfacer.js
@@ -0,0 +1,86 @@
+const EventEmitter = require('events')
+const waitForData = require('./waitForData')
+const ansi = require('./ansi')
+
+module.exports = class CommandLineInterfacer extends EventEmitter {
+  constructor(inStream = process.stdin, outStream = process.stdout) {
+    super()
+
+    this.inStream = inStream
+    this.outStream = outStream
+
+    inStream.on('data', buffer => {
+      this.emit('inputData', buffer)
+    })
+
+    inStream.setRawMode(true)
+  }
+
+  async getScreenSize() {
+    const waitUntil = cond => waitForData(this.inStream, cond)
+
+    // Get old cursor position..
+    this.outStream.write(ansi.requestCursorPosition())
+    const { options: oldCoords } = this.parseANSICommand(
+      await waitUntil(buf => ansi.isANSICommand(buf, 82))
+    )
+
+    // Move far to the bottom right of the screen, then get cursor position..
+    // (We could use moveCursor here, but the 0-index offset isn't really
+    // relevant.)
+    this.outStream.write(ansi.moveCursorRaw(9999, 9999))
+    this.outStream.write(ansi.requestCursorPosition())
+    const { options: sizeCoords } = this.parseANSICommand(
+      await waitUntil(buf => ansi.isANSICommand(buf, 82))
+    )
+
+    // Restore to old cursor position.. (Using moveCursorRaw is actaully
+    // necessary here, since we'll be passing the coordinates returned from
+    // another ANSI command.)
+    this.outStream.write(ansi.moveCursorRaw(oldCoords[0], oldCoords[1]))
+
+    // And return dimensions.
+    const [ sizeLine, sizeCol ] = sizeCoords
+    return {
+      lines: sizeLine, cols: sizeCol,
+      width: sizeCol, height: sizeLine
+    }
+  }
+
+  parseANSICommand(buffer) {
+    // Typically ANSI commands are written ESC[1;2;3;4C
+    // ..where ESC is the ANSI escape code, equal to hexadecimal 1B and
+    //   decimal 33
+    // ..where [ and ; are the literal strings "[" and ";"
+    // ..where 1, 2, 3, and 4 are decimal integer arguments written in ASCII
+    //   that may last more than one byte (e.g. "15")
+    // ..where C is some number representing the code of the command
+
+    if (buffer[0] !== 0x1b || buffer[1] !== 0x5b) {
+      throw new Error('Not an ANSI command')
+    }
+
+    const options = []
+    let curOption = ''
+    let commandCode = null
+    for (let val of buffer.slice(2)) {
+      if (48 <= val && val <= 57) { // 0124356789
+        curOption = curOption.concat(val - 48)
+      } else {
+        options.push(parseInt(curOption))
+        curOption = ''
+
+        if (val !== 59) { // ;
+          commandCode = val
+          break
+        }
+      }
+    }
+
+    return {code: commandCode, options: options}
+  }
+
+  write(data) {
+    this.outStream.write(data)
+  }
+}
diff --git a/Flushable.js b/util/Flushable.js
index 0e73b6d..b031677 100644
--- a/Flushable.js
+++ b/util/Flushable.js
@@ -16,6 +16,8 @@ module.exports = class Flushable {
     this.screenCols = 80
 
     this.ended = false
+    this.paused = false
+    this.requestedFlush = false
 
     this.chunks = []
   }
@@ -25,6 +27,13 @@ module.exports = class Flushable {
   }
 
   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
@@ -55,6 +64,18 @@ module.exports = class Flushable {
     this.chunks = []
   }
 
+  pause() {
+    this.paused = true
+  }
+
+  resume() {
+    this.paused = false
+
+    if (this.requestedFlush) {
+      this.flush()
+    }
+  }
+
   end() {
     this.ended = true
   }
diff --git a/util/TelnetInterfacer.js b/util/TelnetInterfacer.js
new file mode 100644
index 0000000..f9b1c23
--- /dev/null
+++ b/util/TelnetInterfacer.js
@@ -0,0 +1,137 @@
+const ansi = require('./ansi')
+const waitForData = require('./waitForData')
+const EventEmitter = require('events')
+
+module.exports = class TelnetInterfacer extends EventEmitter {
+  constructor(socket) {
+    super()
+
+    this.socket = socket
+
+    socket.on('data', buffer => {
+      if (buffer[0] === 255) {
+        this.handleTelnetData(buffer)
+      } else {
+        this.emit('inputData', buffer)
+      }
+    })
+
+    this.initTelnetOptions()
+  }
+
+  initTelnetOptions() {
+    // Initializes various socket options, using telnet magic.
+
+    // Disables linemode.
+    this.socket.write(Buffer.from([
+      255, 253, 34,  // IAC DO LINEMODE
+      255, 250, 34, 1, 0, 255, 240,  // IAC SB LINEMODE MODE 0 IAC SE
+      255, 251, 1    // IAC WILL ECHO
+    ]))
+
+    // Will SGA. Helps with putty apparently.
+    this.socket.write(Buffer.from([
+      255, 251, 3  // IAC WILL SGA
+    ]))
+
+    this.socket.write(ansi.hideCursor())
+  }
+
+  cleanTelnetOptions() {
+    // Resets the telnet options and magic set in initTelnetOptions.
+
+    this.socket.write(ansi.resetAttributes())
+    this.socket.write(ansi.showCursor())
+  }
+
+  async getScreenSize() {
+    this.socket.write(Buffer.from([255, 253, 31])) // IAC DO NAWS
+
+    let didWillNAWS = false
+    let didSBNAWS = false
+    let sb
+
+    inputLoop: while (true) {
+      const data = await waitForData(this.socket)
+
+      for (let command of this.parseTelnetCommands(data)) {
+        // WILL NAWS
+        if (command[1] === 251 && command[2] === 31) {
+          didWillNAWS = true
+          continue
+        }
+
+        // SB NAWS
+        if (didWillNAWS && command[1] === 250 && command[2] === 31) {
+          didSBNAWS = true
+          sb = command.slice(3)
+          continue
+        }
+
+        // SE
+        if (didSBNAWS && command[1] === 240) { // SE
+          break inputLoop
+        }
+      }
+    }
+
+    return this.parseSBNAWS(sb)
+  }
+
+  parseTelnetCommands(buffer) {
+    if (buffer[0] === 255) {
+      // Telnet IAC (Is A Command) - ignore
+
+      // Split the data into multiple IAC commands if more than one IAC was
+      // sent.
+      const values = Array.from(buffer.values())
+      const commands = []
+      const curCmd = [255]
+      for (let value of values) {
+        if (value === 255) { // IAC
+          commands.push(Array.from(curCmd))
+          curCmd.splice(1, curCmd.length)
+          continue
+        }
+        curCmd.push(value)
+      }
+      commands.push(curCmd)
+
+      return commands
+    } else {
+      return []
+    }
+  }
+
+  write(data) {
+    this.socket.write(data)
+  }
+
+  handleTelnetData(buffer) {
+    let didSBNAWS = false
+    let sbNAWS
+
+    for (let command of this.parseTelnetCommands(buffer)) {
+      // SB NAWS
+      if (command[1] === 250 && command[2] === 31) {
+        didSBNAWS = true
+        sbNAWS = command.slice(3)
+        continue
+      }
+
+      // SE
+      if (didSBNAWS && command[1] === 240) { // SE
+        didSBNAWS = false
+        this.emit('screenSizeUpdated', this.parseSBNAWS(sbNAWS))
+        continue
+      }
+    }
+  }
+
+  parseSBNAWS(sb) {
+    const cols = (sb[0] << 8) + sb[1]
+    const lines = (sb[2] << 8) + sb[3]
+
+    return { cols, lines, width: cols, height: lines }
+  }
+}
diff --git a/ansi.js b/util/ansi.js
index 1f9a392..0e6e3fe 100644
--- a/ansi.js
+++ b/util/ansi.js
@@ -90,6 +90,21 @@ const ansi = {
     return `${ESC}[7m`
   },
 
+  requestCursorPosition() {
+    // Requests the position of the cursor.
+    // Expect a stdin-result '\ESC[l;cR', where l is the line number (1-based),
+    // c is the column number (also 1-based), and R is the literal character
+    // 'R' (decimal code 82).
+
+    return `${ESC}[6n`
+  },
+
+  isANSICommand(buffer, code = null) {
+    return (
+      buffer[0] === 0x1b && buffer[1] === 0x5b &&
+      (code ? buffer[buffer.length - 1] === code : true)
+    )
+  },
 
 
   interpret(text, scrRows, scrCols) {
diff --git a/exception.js b/util/exception.js
index e88ff99..e88ff99 100644
--- a/exception.js
+++ b/util/exception.js
diff --git a/telchars.js b/util/telchars.js
index 8cf414c..8cf414c 100644
--- a/telchars.js
+++ b/util/telchars.js
diff --git a/unichars.js b/util/unichars.js
index d685890..d685890 100644
--- a/unichars.js
+++ b/util/unichars.js
diff --git a/util/waitForData.js b/util/waitForData.js
new file mode 100644
index 0000000..bf40c52
--- /dev/null
+++ b/util/waitForData.js
@@ -0,0 +1,9 @@
+module.exports = function waitForData(stream, cond = null) {
+  return new Promise(resolve => {
+    stream.on('data', data => {
+      if (cond ? cond(data) : true) {
+        resolve(data)
+      }
+    })
+  })
+}