From 8512c44b6403c126bf961c0fd0c2798d6bfdfcea Mon Sep 17 00:00:00 2001 From: Florrie Date: Sat, 6 Jul 2019 00:16:02 -0300 Subject: Experimental telnet server --- backend.js | 13 +++++++-- client.js | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 88 +++++++++++++++----------------------------------------- telnet-server.js | 67 ++++++++++++++++++++++++++++++++++++++++++ ui.js | 38 ++++++++++++++++-------- 5 files changed, 215 insertions(+), 79 deletions(-) create mode 100644 client.js create mode 100644 telnet-server.js 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() { -- cgit 1.3.0-6-gf8a5