« 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--backend.js13
-rw-r--r--client.js88
-rwxr-xr-xindex.js88
-rw-r--r--telnet-server.js67
-rw-r--r--ui.js38
5 files changed, 215 insertions, 79 deletions
diff --git a/backend.js b/backend.js
index c59121a..35362b3 100644
--- a/backend.js
+++ b/backend.js
@@ -175,6 +175,7 @@ class Backend extends EventEmitter {
     }
 
     recursivelyAddTracks(topItem)
+    this.emitQueueUpdated()
 
     // This is the first new track, if a group was queued.
     const newTrack = items[newTrackIndex]
@@ -249,6 +250,8 @@ class Backend extends EventEmitter {
         items.splice(insertIndex, 0, item)
       }
     }
+
+    this.emitQueueUpdated()
   }
 
   unqueue(topItem, focusItem = null) {
@@ -292,6 +295,7 @@ class Backend extends EventEmitter {
     }
 
     recursivelyUnqueueTracks(topItem)
+    this.emitQueueUpdated()
 
     return focusItem
   }
@@ -323,6 +327,7 @@ class Backend extends EventEmitter {
     const remainingItems = queue.items.slice(index)
     const newItems = initialItems.concat(shuffleArray(remainingItems))
     queue.items = newItems
+    this.emitQueueUpdated()
   }
 
   clearQueue() {
@@ -330,8 +335,12 @@ class Backend extends EventEmitter {
     // the track that's currently playing).
     this.queueGrouplike.items = this.queueGrouplike.items
       .filter(item => item === this.playingTrack)
+    this.emitQueueUpdated()
   }
 
+  emitQueueUpdated() {
+    this.emit('queue updated')
+  }
 
   seekAhead(seconds) {
     this.player.seekAhead(seconds)
@@ -369,11 +378,11 @@ class Backend extends EventEmitter {
     this.player.setVolume(value)
   }
 
-  stopPlaying() {
+  async stopPlaying() {
     // We emit this so playTrack doesn't immediately start a new track.
     // We aren't *actually* about to play a new track.
     this.emit('playing new track')
-    this.player.kill()
+    await this.player.kill()
     this.clearPlayingTrack()
   }
 
diff --git a/client.js b/client.js
new file mode 100644
index 0000000..7f21d1a
--- /dev/null
+++ b/client.js
@@ -0,0 +1,88 @@
+// Generic code for setting up mtui and the UI for any command line client.
+
+'use strict'
+
+const AppElement = require('./ui')
+const processSmartPlaylist = require('./smart-playlist')
+
+const {
+  ui: {
+    Root
+  },
+  util: {
+    ansi,
+    Flushable,
+    TelnetInterfacer
+  }
+} = require('./tui-lib')
+
+const setupClient = async ({backend, writable, interfacer, appConfig}) => {
+  const cleanTerminal = () => {
+    writable.write(ansi.cleanCursor())
+    writable.write(ansi.disableAlternateScreen())
+  }
+
+  const dirtyTerminal = () => {
+    writable.write(ansi.enableAlternateScreen())
+    writable.write(ansi.startTrackingMouse())
+  }
+
+  dirtyTerminal()
+
+  const root = new Root(interfacer)
+
+  const size = await interfacer.getScreenSize()
+  root.w = size.width
+  root.h = size.height
+  root.fixAllLayout()
+
+  const flushable = new Flushable(writable, true)
+  flushable.resizeScreen(size)
+  flushable.write(ansi.clearScreen())
+  flushable.flush()
+
+  interfacer.on('resize', newSize => {
+    root.w = newSize.width
+    root.h = newSize.height
+    flushable.resizeScreen(newSize)
+    root.fixAllLayout()
+  })
+
+  const appElement = new AppElement(backend, appConfig)
+  root.addChild(appElement)
+  root.select(appElement)
+
+  appElement.on('quitRequested', () => {
+    cleanTerminal()
+  })
+
+  appElement.on('suspendRequested', () => {
+    cleanTerminal()
+  })
+
+  // TODO: Don't load a default playlist?
+  let grouplike = {
+    name: 'My ~/Music Library',
+    comment: (
+      '(Add songs and folders to ~/Music to make them show up here,' +
+      ' or pass mtui your own playlist.json file!)'),
+    source: ['crawl-local', process.env.HOME + '/Music']
+  }
+  grouplike = await processSmartPlaylist(grouplike)
+  appElement.tabber.currentElement.loadGrouplike(grouplike)
+
+  root.select(appElement)
+
+  // Load up initial state
+  appElement.queueListingElement.buildItems()
+  appElement.playbackInfoElement.updateTrack(backend.playingTrack)
+
+  const renderInterval = setInterval(() => {
+    root.renderTo(flushable)
+    flushable.flush()
+  }, 100)
+
+  return {appElement, cleanTerminal, renderInterval}
+}
+
+module.exports = setupClient
diff --git a/index.js b/index.js
index 102e75a..10e0696 100755
--- a/index.js
+++ b/index.js
@@ -5,7 +5,9 @@
 const { getAllCrawlersForArg } = require('./crawlers')
 const AppElement = require('./ui')
 const Backend = require('./backend')
+const TelnetServer = require('./telnet-server')
 const processSmartPlaylist = require('./smart-playlist')
+const setupClient = require('./client')
 
 const {
   getItemPathString,
@@ -48,12 +50,14 @@ process.on('unhandledRejection', error => {
 })
 
 async function main() {
-  const interfacer = new CommandLineInterfacer()
-
-  const root = new Root(interfacer)
-
   const backend = new Backend()
 
+  const result = await backend.setup()
+  if (result.error) {
+    console.error(result.error)
+    process.exit(1)
+  }
+
   backend.on('playing', track => {
     if (track) {
       writeFile(backend.rootDirectory + '/current-track.txt',
@@ -63,58 +67,24 @@ async function main() {
     }
   })
 
-  const appElement = new AppElement(backend)
-  root.addChild(appElement)
-  root.select(appElement)
-
-  const result = await backend.setup()
-
-  if (result.error) {
-    console.error(result.error)
-    process.exit(1)
-  }
-
-  const cleanTerminal = () => {
-    process.stdout.write(ansi.cleanCursor())
-    process.stdout.write(ansi.disableAlternateScreen())
-  }
-
-  const dirtyTerminal = () => {
-    process.stdout.write(ansi.enableAlternateScreen())
-    process.stdout.write(ansi.startTrackingMouse())
-  }
+  const { appElement, renderInterval } = await setupClient({
+    backend,
+    interfacer: new CommandLineInterfacer(),
+    writable: process.stdout
+  })
 
   appElement.on('quitRequested', () => {
-    cleanTerminal()
+    if (telnetServer) {
+      telnetServer.disconnectAllSockets('User closed mtui - see you!')
+    }
+    clearInterval(renderInterval)
     process.exit(0)
   })
 
   appElement.on('suspendRequested', () => {
-    cleanTerminal()
     process.kill(process.pid, 'SIGTSTP')
   })
 
-  let grouplike = {
-    name: 'My ~/Music Library',
-    comment: (
-      '(Add songs and folders to ~/Music to make them show up here,' +
-      ' or pass mtui your own playlist.json file!)'),
-    source: ['crawl-local', process.env.HOME + '/Music']
-  }
-
-  grouplike = await processSmartPlaylist(grouplike)
-
-  appElement.tabber.currentElement.loadGrouplike(grouplike)
-
-  root.select(appElement)
-
-  // Check size, now that we're about to display.
-  const size = await interfacer.getScreenSize()
-  root.w = size.width
-  root.h = size.height
-  root.fixAllLayout()
-
-  dirtyTerminal()
   process.on('SIGCONT', () => {
     flushable.resizeScreen({lines: flushable.screenLines, cols: flushable.screenCols})
     process.stdin.setRawMode(false)
@@ -122,19 +92,6 @@ async function main() {
     dirtyTerminal()
   })
 
-  const flushable = new Flushable(process.stdout, true)
-  flushable.resizeScreen(size)
-  flushable.shouldShowCompressionStatistics = process.argv.includes('--show-ansi-stats')
-  flushable.write(ansi.clearScreen())
-  flushable.flush()
-
-  interfacer.on('resize', newSize => {
-    root.w = newSize.width
-    root.h = newSize.height
-    flushable.resizeScreen(newSize)
-    root.fixAllLayout()
-  })
-
   const loadPlaylists = async () => {
     for (let i = 2; i < process.argv.length; i++) {
       if (!process.argv[i].startsWith('--')) {
@@ -145,6 +102,12 @@ async function main() {
 
   const loadPlaylistPromise = loadPlaylists()
 
+  let telnetServer
+  if (process.argv.includes('--telnet-server')) {
+    telnetServer = new TelnetServer(backend)
+    await telnetServer.listen(1244)
+  }
+
   if (process.argv.includes('--stress-test')) {
     await loadPlaylistPromise
 
@@ -191,11 +154,6 @@ async function main() {
 
     return
   }
-
-  setInterval(() => {
-    root.renderTo(flushable)
-    flushable.flush()
-  }, 50)
 }
 
 main().catch(err => {
diff --git a/telnet-server.js b/telnet-server.js
new file mode 100644
index 0000000..6cee554
--- /dev/null
+++ b/telnet-server.js
@@ -0,0 +1,67 @@
+'use strict'
+
+const net = require('net')
+const setupClient = require('./client')
+
+const {
+  util: {
+    TelnetInterfacer
+  }
+} = require('./tui-lib')
+
+class TelnetServer {
+  constructor(backend) {
+    this.backend = backend
+    this.server = new net.Server(socket => this.handleConnection(socket))
+    this.sockets = []
+  }
+
+  listen(port) {
+    this.server.listen(port)
+  }
+
+  async handleConnection(socket) {
+    const interfacer = new TelnetInterfacer(socket)
+    const { appElement, renderInterval, cleanTerminal } = await setupClient({
+      backend: this.backend,
+      writable: socket,
+      interfacer,
+      appConfig: {
+        stopPlayingUponQuit: false,
+        menubarColor: 2
+      }
+    })
+
+    let closed = false
+
+    const quit = (msg = 'See you!') => {
+      cleanTerminal()
+      interfacer.cleanTelnetOptions()
+      socket.write('\r' + msg + '\r\n')
+      socket.end()
+      this.sockets.splice(this.sockets.indexOf(socket, 1))
+      closed = true
+    }
+
+    appElement.on('quitRequested', quit)
+
+    socket.on('close', () => {
+      if (!closed) {
+        clearInterval(renderInterval)
+        this.sockets.splice(this.sockets.indexOf(socket, 1))
+        closed = true
+      }
+    })
+
+    socket.quit = quit
+    this.sockets.push(socket)
+  }
+
+  disconnectAllSockets(msg) {
+    for (const socket of this.sockets) {
+      socket.quit(msg)
+    }
+  }
+}
+
+module.exports = TelnetServer
diff --git a/ui.js b/ui.js
index c2c3e67..4f73fe0 100644
--- a/ui.js
+++ b/ui.js
@@ -134,11 +134,16 @@ telc.isSelect = input.isSelect
 telc.isBackspace = input.isBackspace
 
 class AppElement extends FocusElement {
-  constructor(backend) {
+  constructor(backend, config = {}) {
     super()
 
     this.backend = backend
 
+    this.config = Object.assign({
+      menubarColor: 4, // blue
+      stopPlayingUponQuit: true
+    }, config)
+
     this.backend.on('playing', (track, oldTrack) => {
       if (track) {
         this.playbackInfoElement.updateTrack(track)
@@ -160,6 +165,10 @@ class AppElement extends FocusElement {
       this.metadataStatusLabel.text = `Processing metadata - ${remaining} to go.`
     })
 
+    this.backend.on('queue updated', () => {
+      this.queueListingElement.buildItems()
+    })
+
     // TODO: Move edit mode stuff to the backend!
     this.undoManager = new UndoManager()
     this.markGrouplike = {name: 'Marked', items: []}
@@ -171,6 +180,8 @@ class AppElement extends FocusElement {
     this.menubar = new Menubar(this.menu)
     this.addChild(this.menubar)
 
+    this.menubar.color = this.config.menubarColor
+
     this.paneLeft = new Pane()
     this.addChild(this.paneLeft)
 
@@ -571,7 +582,10 @@ class AppElement extends FocusElement {
   }
 
   async shutdown() {
-    await this.backend.stopPlaying()
+    if (this.config.stopPlayingUponQuit) {
+      await this.backend.stopPlaying()
+    }
+
     this.emit('quitRequested')
   }
 
@@ -708,12 +722,10 @@ class AppElement extends FocusElement {
 
   shuffleQueue() {
     this.backend.shuffleQueue()
-    this.queueListingElement.buildItems()
   }
 
   clearQueue() {
     this.backend.clearQueue()
-    this.queueListingElement.buildItems()
     this.queueListingElement.selectNone()
     this.updateQueueLengthLabel()
 
@@ -756,7 +768,6 @@ class AppElement extends FocusElement {
       this.backend.queue(item, afterItem, {
         movePlayingTrack: order === 'normal'
       })
-      this.queueListingElement.buildItems()
 
       if (isTrack(passedItem)) {
         this.queueListingElement.selectAndShow(passedItem)
@@ -765,7 +776,6 @@ class AppElement extends FocusElement {
       this.backend.distributeQueue(item, {
         how: where.slice('distribute-'.length)
       })
-      this.queueListingElement.buildItems()
     }
 
     this.updateQueueLengthLabel()
@@ -1965,12 +1975,16 @@ class PlaybackInfoElement extends DisplayElement {
   }
 
   updateTrack(track) {
-    this.currentTrack = track
-    this.trackNameLabel.text = track.name
-    this.progressBarLabel.text = ''
-    this.progressTextLabel.text = '(Starting..)'
-    this.timeData = {}
-    this.fixLayout()
+    if (track) {
+      this.currentTrack = track
+      this.trackNameLabel.text = track.name
+      this.progressBarLabel.text = ''
+      this.progressTextLabel.text = '(Starting..)'
+      this.timeData = {}
+      this.fixLayout()
+    } else {
+      this.clearInfo()
+    }
   }
 
   clearInfo() {