« 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.js5
-rw-r--r--socket.js133
-rw-r--r--ui.js49
3 files changed, 162 insertions, 25 deletions
diff --git a/backend.js b/backend.js
index fe7014b..e75b1a8 100644
--- a/backend.js
+++ b/backend.js
@@ -283,6 +283,11 @@ class QueuePlayer extends EventEmitter {
     return focusItem
   }
 
+  replaceAllItems(newItems) {
+    this.queueGrouplike.items = newItems
+    this.emitQueueUpdated()
+  }
+
   clearQueuePast(track) {
     const { items } = this.queueGrouplike
     const index = items.indexOf(track) + 1
diff --git a/socket.js b/socket.js
index 12d08af..aea2ee8 100644
--- a/socket.js
+++ b/socket.js
@@ -22,9 +22,15 @@ import {
 } from './serialized-backend.js'
 
 import {
-  silenceEvents
+  getTimeStringsFromSec,
+  silenceEvents,
 } from './general-util.js'
 
+// This is expected to be the same across both the client and the server.
+// There will probably be inconsistencies between sender clients and receiving
+// clients / the server otherwise.
+const DEFAULT_NICKNAME = '(Unnamed)'
+
 function serializeCommandToData(command) {
   // Turn a command into a string/buffer that can be sent over a socket.
   return JSON.stringify(command)
@@ -96,7 +102,7 @@ function validateCommand(command) {
         case 'clear-queue-up-to':
           return (
             typeof command.queuePlayer === 'string' &&
-            isItemRef(command.topItem)
+            isItemRef(command.track)
           )
         case 'distribute-queue':
           return (
@@ -138,7 +144,8 @@ function validateCommand(command) {
           return (
             typeof command.queuePlayer === 'string' &&
             Array.isArray(command.tracks) &&
-            command.tracks.every(track => isItemRef(track))
+            command.tracks.every(track => isItemRef(track)) &&
+            ['shuffle'].includes(command.why)
           )
         case 'seek-to':
           return (
@@ -148,6 +155,7 @@ function validateCommand(command) {
         case 'set-nickname':
           return (
             typeof command.nickname === 'string' &&
+            typeof command.oldNickname === 'string' &&
             command.nickname.length >= 1 &&
             command.nickname.length <= 12
           )
@@ -225,7 +233,7 @@ export function makeSocketServer() {
   server.on('connection', socket => {
     sockets.push(socket)
 
-    let nickname = '(Unnamed)'
+    let nickname = DEFAULT_NICKNAME
 
     socket.on('close', () => {
       if (sockets.includes(socket)) {
@@ -261,7 +269,7 @@ export function makeSocketServer() {
               readySockets.push(socket)
               if (readySockets.length === sockets.length) {
                 for (const socket of sockets) {
-                  socket.write(JSON.stringify({
+                  socket.write(serializeCommandToData({
                     sender: 'server',
                     code: 'set-pause',
                     queuePlayer: command.queuePlayer,
@@ -277,13 +285,13 @@ export function makeSocketServer() {
           case 'sync-playback':
             for (const QP of server.canonicalBackend.queuePlayers) {
               if (QP.timeData) {
-                socket.write(JSON.stringify({
+                socket.write(serializeCommandToData({
                   sender: 'server',
                   code: 'seek-to',
                   queuePlayer: QP.id,
                   time: QP.timeData.curSecTotal
                 }) + '\n')
-                socket.write(JSON.stringify({
+                socket.write(serializeCommandToData({
                   sender: 'server',
                   code: 'set-pause',
                   queuePlayer: QP.id,
@@ -304,8 +312,11 @@ export function makeSocketServer() {
       }
 
       // If it's a 'set-nickname' command, save the nickname.
+      // Also attach the old nickname for display in log messages.
 
       if (command.code === 'set-nickname') {
+        command.oldNickname = nickname
+        command.senderNickname = nickname
         nickname = command.nickname
       }
 
@@ -314,7 +325,7 @@ export function makeSocketServer() {
       const otherSockets = sockets.filter(s => s !== socket)
 
       for (const socket of otherSockets) {
-        socket.write(JSON.stringify(command) + '\n')
+        socket.write(serializeCommandToData(command) + '\n')
       }
     }))
 
@@ -326,7 +337,7 @@ export function makeSocketServer() {
       }
     }
 
-    socket.write(JSON.stringify({
+    socket.write(serializeCommandToData({
       sender: 'server',
       code: 'initialize-backend',
       backend: savedBackend
@@ -343,6 +354,7 @@ export function makeSocketClient() {
 
   const client = new EventEmitter()
   client.socket = new net.Socket()
+  client.nickname = DEFAULT_NICKNAME
 
   client.sendCommand = function(command) {
     const data = serializeCommandToData(command)
@@ -351,7 +363,9 @@ export function makeSocketClient() {
   }
 
   client.setNickname = function(nickname) {
-    client.sendCommand({code: 'set-nickname', nickname})
+    let oldNickname = client.nickname
+    client.nickname = nickname
+    client.sendCommand({code: 'set-nickname', nickname, oldNickname})
   }
 
   client.socket.on('data', perLine(line => {
@@ -384,11 +398,78 @@ export function attachBackendToSocketClient(backend, client, {
   backend.setAlwaysStartPaused(true)
 
   function logCommand(command) {
-    const nickname = command.sender === 'server' ? 'the server' : command.nickname
-    backend.showLogMessage(`${nickname} sent ${command.code}!`)
+    const nickToMessage = nickname => `\x1b[32;1m${nickname}\x1b[0m`
+    const itemToMessage = item => `\x1b[32m"${item.name}"\x1b[0m`
+    let fullmsg = '' // may be overridden
+    let actionmsg = `sent ${command.code}` // fallback
+    switch (command.code) {
+      case 'clear-queue':
+        actionmsg = 'cleared the queue'
+        break
+      case 'clear-queue-past':
+        actionmsg = `cleared the queue past ${itemToMessage(command.track)}`
+        break
+      case 'clear-queue-up-to':
+        actionmsg = `cleared the queue up to ${itemToMessage(command.track)}`
+        break
+      case 'distribute-queue':
+        actionmsg = `distributed ${itemToMessage(command.topItem)} across the queue ${command.opts.how}`
+        break
+      case 'initialize-backend':
+        return
+      case 'play':
+        actionmsg = `started playing ${itemToMessage(command.track)}`
+        break
+      case 'queue': {
+        let afterMessage = ''
+        if (isItemRef(command.afterItem)) {
+          afterMessage = ` after ${itemToMessage(command.afterItem)}`
+        } else if (command.afterItem === 'FRONT') {
+          afterMessage = ` at the front of the queue`
+        }
+        actionmsg = `queued ${itemToMessage(command.topItem)}` + afterMessage
+        break
+      }
+      case 'restore-queue':
+        if (command.why === 'shuffle') {
+          actionmsg = 'shuffled the queue'
+        }
+        break
+      case 'seek-to':
+        // TODO: the second value here should be the duration of the track
+        // (this will make values like 0:0x:yy / 1:xx:yy appear correctly)
+        actionmsg = `seeked to ${getTimeStringsFromSec(command.time, command.time).timeDone}`
+        break
+      case 'set-nickname':
+        fullmsg = `${nickToMessage(command.nickname)} updated their nickname (from ${nickToMessage(command.oldNickname)})`
+        break
+      case 'set-pause':
+        if (command.paused) {
+          actionmsg = 'paused the player'
+        } else {
+          actionmsg = 'resumed the player'
+        }
+        break
+      case 'stop-playing':
+        actionmsg = 'stopped the player'
+        break
+      case 'unqueue':
+        actionmsg = `removed ${itemToMessage(command.topItem)} from the queue`
+        break
+      case 'status':
+        return
+    }
+    if (!fullmsg) {
+      const nickname = command.sender === 'server' ? 'the server' : command.senderNickname
+      fullmsg = `${nickToMessage(nickname)} ${actionmsg}`
+    }
+    backend.showLogMessage(fullmsg)
   }
 
-  client.on('sent-command', logCommand)
+  client.on('sent-command', command => {
+    command.senderNickname = client.nickname
+    logCommand(command)
+  })
 
   client.on('command', async command => {
     logCommand(command)
@@ -419,17 +500,17 @@ export function attachBackendToSocketClient(backend, client, {
             return
           case 'clear-queue-past':
             if (QP) silenceEvents(QP, ['clear-queue-past'], () => QP.clearQueuePast(
-              restoreNewItem(command.topItem, getPlaylistSources())
+              restoreNewItem(command.track, getPlaylistSources())
             ))
             return
           case 'clear-queue-up-to':
             if (QP) silenceEvents(QP, ['clear-queue-up-to'], () => QP.clearQueueUpTo(
-              restoreNewItem(command.topItem, getPlaylistSources())
+              restoreNewItem(command.track, getPlaylistSources())
             ))
             return
           case 'distribute-queue':
             if (QP) silenceEvents(QP, ['distribute-queue'], () => QP.distributeQueue(
-              restoreNewItem(command.topItem, getPlaylistSources()),
+              restoreNewItem(command.topItem),
               {
                 how: command.opts.how,
                 rangeEnd: command.opts.rangeEnd
@@ -452,7 +533,7 @@ export function attachBackendToSocketClient(backend, client, {
             return
           case 'queue':
             if (QP) silenceEvents(QP, ['queue'], () => QP.queue(
-              restoreNewItem(command.topItem, getPlaylistSources()),
+              restoreNewItem(command.topItem),
               isItemRef(command.afterItem) ? restoreNewItem(command.afterItem, getPlaylistSources()) : command.afterItem,
               {
                 movePlayingTrack: command.opts.movePlayingTrack
@@ -461,14 +542,17 @@ export function attachBackendToSocketClient(backend, client, {
             return
           case 'restore-queue':
             if (QP) {
-              QP.queueGrouplike.items = command.tracks.map(refData => restoreNewItem(refData))
-              // TODO: target just the one queue player. hacks = illegal
-              updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())
+              QP.replaceAllItems(command.tracks.map(
+                refData => restoreNewItem(refData, getPlaylistSources())
+              ))
             }
+            return
           case 'seek-to':
             if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time))
             return
           case 'set-pause': {
+            // TODO: there's an event leak here when toggling pause while
+            // nothing is playing
             let playingThisTrack = true
             QP.once('playing new track', () => {
               playingThisTrack = false
@@ -500,19 +584,19 @@ export function attachBackendToSocketClient(backend, client, {
     })
   })
 
-  backend.on('clear-queue-past', (queuePlayer, topItem) => {
+  backend.on('clear-queue-past', (queuePlayer, track) => {
     client.sendCommand({
       code: 'clear-queue-past',
       queuePlayer: queuePlayer.id,
-      topItem: saveItemReference(topItem)
+      track: saveItemReference(track)
     })
   })
 
-  backend.on('clear-queue-up-to', (queuePlayer, topItem) => {
+  backend.on('clear-queue-up-to', (queuePlayer, track) => {
     client.sendCommand({
       code: 'clear-queue-up-to',
       queuePlayer: queuePlayer.id,
-      topItem: saveItemReference(topItem)
+      track: saveItemReference(track)
     })
   })
 
@@ -572,6 +656,7 @@ export function attachBackendToSocketClient(backend, client, {
   backend.on('shuffle-queue', queuePlayer => {
     client.sendCommand({
       code: 'restore-queue',
+      why: 'shuffle',
       queuePlayer: queuePlayer.id,
       tracks: queuePlayer.queueGrouplike.items.map(saveItemReference)
     })
diff --git a/ui.js b/ui.js
index 05e359e..df02edf 100644
--- a/ui.js
+++ b/ui.js
@@ -5530,6 +5530,10 @@ class Log extends ListScrollForm {
   }
 
   newLogMessage(text) {
+    if (this.inputs.length === 10) {
+      this.removeInput(this.inputs[0])
+    }
+
     const logMessage = new LogMessage(text)
     this.addInput(logMessage)
     this.fixLayout()
@@ -5538,4 +5542,47 @@ class Log extends ListScrollForm {
   }
 }
 
-class LogMessage extends Button {}
+class LogMessage extends FocusElement {
+  constructor(text) {
+    super()
+
+    this.label = new LogMessageLabel(text)
+    this.addChild(this.label)
+  }
+
+  fixLayout() {
+    this.w = this.parent.contentW
+    this.label.w = this.contentW
+    this.h = this.label.h
+  }
+
+  clicked(button) {
+    if (button === 'left') {
+      this.root.select(this)
+      return false
+    }
+  }
+}
+
+class LogMessageLabel extends WrapLabel {
+  writeTextTo(writable) {
+    const w = this.w
+    const lines = this.getWrappedLines()
+    for (let i = 0; i < lines.length; i++) {
+      const text = this.processFormatting(lines[i])
+      writable.write(ansi.moveCursor(this.absTop + i, this.absLeft))
+      writable.write(text)
+      const width = ansi.measureColumns(text)
+      if (width < w && this.textAttributes.length) {
+        writable.write(ansi.setAttributes([ansi.A_RESET, ...this.textAttributes]))
+        writable.write(' '.repeat(w - width))
+      }
+    }
+  }
+
+  set textAttributes(val) {}
+
+  get textAttributes() {
+    return this.parent.isSelected ? [40] : []
+  }
+}