« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--index.js49
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--record-store.js35
m---------tui-lib0
-rw-r--r--ui.js113
6 files changed, 203 insertions, 0 deletions
diff --git a/index.js b/index.js
index f85d101..a86918a 100644
--- a/index.js
+++ b/index.js
@@ -2,7 +2,12 @@
 
 const { getPlayer } = require('./players')
 const { getDownloaderFor } = require('./downloaders')
+const { AppElement } = require('./ui')
+const ansi = require('./tui-lib/util/ansi')
+const CommandLineInterfacer = require('./tui-lib/util/CommandLineInterfacer')
 const EventEmitter = require('events')
+const Flushable = require('./tui-lib/util/Flushable')
+const Root = require('./tui-lib/ui/Root')
 
 class InternalApp extends EventEmitter {
   constructor() {
@@ -53,9 +58,53 @@ async function main() {
   internalApp.stopPlaying()
   */
 
+  /*
   for (const item of require('./flat.json').items) {
     await internalApp.download(item.downloaderArg)
   }
+  */
+
+  const interfacer = new CommandLineInterfacer()
+  const size = await interfacer.getScreenSize()
+
+  const flushable = new Flushable(process.stdout, true)
+  flushable.screenLines = size.lines
+  flushable.screenCols = size.cols
+  flushable.shouldShowCompressionStatistics = process.argv.includes('--show-ansi-stats')
+  flushable.write(ansi.clearScreen())
+  flushable.flush()
+
+  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.stdout.write(ansi.cleanCursor())
+    process.exit(0)
+  })
+
+  const grouplike = {
+    items: [
+      {name: 'Nice'},
+      {name: 'W00T!'},
+      {name: 'All-star'}
+    ]
+  }
+
+  appElement.recordStore.getRecord(grouplike.items[2]).downloading = true
+
+  appElement.grouplikeListingElement.loadGrouplike(grouplike)
+
+  root.select(appElement.grouplikeListingElement)
+
+  setInterval(() => {
+    root.renderTo(flushable)
+    flushable.flush()
+  }, 50)
 }
 
 main().catch(err => console.error(err))
diff --git a/package-lock.json b/package-lock.json
index f1205d9..e9fd90f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,11 @@
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
       "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
     },
+    "iac": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/iac/-/iac-1.1.0.tgz",
+      "integrity": "sha1-C83Rc3Jy/qwj5126pFeCnYHTtMw="
+    },
     "js-base64": {
       "version": "2.4.5",
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
diff --git a/package.json b/package.json
index ec51a8b..b26650a 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
     "command-exists": "^1.2.6",
     "fifo-js": "^2.1.0",
     "fs-extra": "^6.0.1",
+    "iac": "^1.1.0",
     "js-base64": "^2.4.5",
     "mkdirp": "^0.5.1",
     "node-fetch": "^2.1.2",
diff --git a/record-store.js b/record-store.js
new file mode 100644
index 0000000..d0c7623
--- /dev/null
+++ b/record-store.js
@@ -0,0 +1,35 @@
+const recordSymbolKey = Symbol()
+
+module.exports = class RecordStore {
+  constructor() {
+    // Each track (or whatever) gets a symbol which is used as a key here to
+    // store more information.
+    this.data = {}
+  }
+
+  getRecord(obj) {
+    if (typeof obj !== 'object') {
+      throw new TypeError('Cannot get the record of a non-object')
+    }
+
+    if (!obj[recordSymbolKey]) {
+      obj[recordSymbolKey] = Symbol()
+    }
+
+    if (!this.data[obj[recordSymbolKey]]) {
+      this.data[obj[recordSymbolKey]] = {}
+    }
+
+    return this.data[obj[recordSymbolKey]]
+  }
+
+  deleteRecord(obj) {
+    if (typeof obj !== 'object') {
+      throw new TypeError('Non-objects cannot have a record in the first place')
+    }
+
+    if (obj[recordSymbolKey]) {
+      delete this.data[obj[recordSymbolKey]]
+    }
+  }
+}
diff --git a/tui-lib b/tui-lib
-Subproject 6ee1936266dda3bd22e4412a7b51cdc6e3c396d
+Subproject 581c8db27bc25c74b02a1b29d795c847118c623
diff --git a/ui.js b/ui.js
new file mode 100644
index 0000000..26aa12e
--- /dev/null
+++ b/ui.js
@@ -0,0 +1,113 @@
+const ansi = require('./tui-lib/util/ansi')
+const Button = require('./tui-lib/ui/form/Button')
+const FocusElement = require('./tui-lib/ui/form/FocusElement')
+const ListScrollForm = require('./tui-lib/ui/form/ListScrollForm')
+const Pane = require('./tui-lib/ui/Pane')
+const RecordStore = require('./record-store')
+
+class AppElement extends FocusElement {
+  constructor(internalApp) {
+    super()
+
+    this.internalApp = internalApp
+    this.recordStore = new RecordStore()
+
+    this.pane = new Pane()
+    this.addChild(this.pane)
+
+    this.grouplikeListingElement = new GrouplikeListingElement(this.recordStore)
+    this.pane.addChild(this.grouplikeListingElement)
+  }
+
+  fixLayout() {
+    this.w = this.parent.contentW
+    this.h = this.parent.contentH
+
+    this.pane.w = this.contentW
+    this.pane.h = this.contentH
+
+    this.grouplikeListingElement.w = this.pane.contentW
+    this.grouplikeListingElement.h = this.pane.contentH
+  }
+
+  keyPressed(keyBuf) {
+    if (keyBuf[0] === 0x03) { // ^C
+      this.emit('quitRequested')
+      return
+    }
+
+    super.keyPressed(keyBuf)
+  }
+}
+
+class GrouplikeListingElement extends ListScrollForm {
+  constructor(recordStore) {
+    super('vertical')
+
+    this.grouplike = null
+    this.recordStore = recordStore
+  }
+
+  loadGrouplike(grouplike) {
+    this.grouplike = grouplike
+    this.buildItems()
+  }
+
+  buildItems() {
+    if (!this.grouplike) {
+      throw new Error('Attempted to call buildItems before a grouplike was loaded')
+    }
+
+    for (const item of this.grouplike.items) {
+      this.addInput(new GrouplikeItemElement(item, this.recordStore))
+    }
+
+    this.fixLayout()
+  }
+}
+
+class GrouplikeItemElement extends Button {
+  constructor(item, recordStore) {
+    super()
+
+    this.item = item
+    this.recordStore = recordStore
+  }
+
+  fixLayout() {
+    this.w = this.parent.contentW
+    this.h = 1
+  }
+
+  drawTo(writable) {
+    if (this.isFocused) {
+      writable.write(ansi.invert())
+    }
+
+    writable.write(ansi.moveCursor(this.absTop, this.absLeft))
+    this.drawX = this.x
+    this.writeStatus(writable)
+    this.drawX += this.item.name.length
+    writable.write(this.item.name)
+    writable.write(' '.repeat(this.w - this.drawX))
+
+    writable.write(ansi.resetAttributes())
+  }
+
+  writeStatus(writable) {
+    this.drawX += 3
+
+    const braille = '⠈⠐⠠⠄⠂⠁'
+    const brailleChar = braille[Math.floor(Date.now() / 250) % 6]
+
+    writable.write(' ')
+    if (this.recordStore.getRecord(this.item).downloading) {
+      writable.write(braille[Math.floor(Date.now() / 250) % 6])
+    } else {
+      writable.write(' ')
+    }
+    writable.write(' ')
+  }
+}
+
+module.exports.AppElement = AppElement