« 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--ui.js48
-rw-r--r--undo-manager.js42
2 files changed, 87 insertions, 3 deletions
diff --git a/ui.js b/ui.js
index 4b64017..c4c2307 100644
--- a/ui.js
+++ b/ui.js
@@ -4,6 +4,7 @@ const { getPlayer } = require('./players')
 const { parentSymbol, isGroup, isTrack, getItemPath, getItemPathString, flattenGrouplike } = require('./playlist-utils')
 const { shuffleArray } = require('./general-util')
 const processSmartPlaylist = require('./smart-playlist')
+const UndoManager = require('./undo-manager')
 
 const ansi = require('./tui-lib/util/ansi')
 const Button = require('./tui-lib/ui/form/Button')
@@ -30,7 +31,9 @@ class AppElement extends FocusElement {
 
     this.player = null
     this.recordStore = new RecordStore()
+    this.undoManager = new UndoManager()
     this.queueGrouplike = {isTheQueue: true, items: []}
+    this.editMode = false
 
     this.rootDirectory = process.env.HOME + '/.mtui'
 
@@ -57,7 +60,8 @@ class AppElement extends FocusElement {
     this.queueListingElement.on('select (enter)', item => this.playGrouplikeItem(item, false))
     this.queueListingElement.on('select (space)', item => this.handleSpacePressed(
       () => this.playGrouplikeItem(item, false)))
-    this.queueListingElement.on('remove', item => this.unqueueGrouplikeItem(item))
+    this.queueListingElement.on('remove (backspace)', item => this.unqueueGrouplikeItem(item))
+    this.queueListingElement.on('remove (x)', item => this.unqueueGrouplikeItem(item))
     this.queueListingElement.on('shuffle', () => this.shuffleQueue())
     this.queueListingElement.on('clear', () => this.clearQueue())
     this.queueListingElement.on('select main listing',
@@ -109,6 +113,36 @@ class AppElement extends FocusElement {
     grouplikeListing.on('queue (shuffled)', item => this.shuffleQueueGrouplikeItem(item))
     grouplikeListing.on('queue (play next)', item => this.queueGrouplikeItem(item, true, this.playingTrack))
 
+    grouplikeListing.on('remove (x)', item => {
+      if (this.editMode) {
+        const parent = item[parentSymbol]
+        const index = parent.items.indexOf(item)
+
+        const updateDisplay = () => {
+          for (const grouplikeListing of this.tabber.tabberElements) {
+            if (grouplikeListing.grouplike === parent) {
+              grouplikeListing.loadGrouplike(parent)
+            } else if (grouplikeListing.grouplike === item) {
+              grouplikeListing.loadGrouplike(item)
+            }
+          }
+        }
+
+        this.undoManager.pushAction({
+          activate: () => {
+            parent.items.splice(index, 1)
+            delete item[parentSymbol]
+            updateDisplay()
+          },
+          undo: () => {
+            parent.items.splice(index, 0, item)
+            item[parentSymbol] = parent
+            updateDisplay()
+          }
+        })
+      }
+    })
+
     const handleSelectFromPathElement = item => {
       this.form.selectInput(grouplikeListing)
       if (isGroup(item)) {
@@ -241,12 +275,18 @@ class AppElement extends FocusElement {
     } else if (telc.isCharacter(keyBuf, '2') && this.queueListingElement.selectable) {
       this.form.curIndex = this.form.inputs.indexOf(this.queueListingElement)
       this.form.updateSelectedElement()
+    } else if (keyBuf.equals(Buffer.from([5]))) { // Ctrl-E
+      this.editMode = !this.editMode
     } else if (keyBuf.equals(Buffer.from([15]))) { // ctrl-O
       this.openPlaylistDialog.open()
     } else if (keyBuf.equals(Buffer.from([20]))) { // ctrl-T
       this.cloneCurrentTab()
     } else if (keyBuf.equals(Buffer.from([23]))) { // ctrl-W
       this.tabber.closeTab(this.tabber.currentElement)
+    } else if (keyBuf.equals(Buffer.from(['j'.charCodeAt(0)]))) {
+      this.undoManager.undoLastAction()
+    } else if (keyBuf.equals(Buffer.from(['J'.charCodeAt(0)]))) {
+      this.undoManager.redoLastUndoneAction()
     } else if (this.tabber.isSelected && keyBuf.equals(Buffer.from(['t'.charCodeAt(0)]))) {
       this.tabber.nextTab()
     } else if (this.tabber.isSelected && keyBuf.equals(Buffer.from(['T'.charCodeAt(0)]))) {
@@ -634,7 +674,7 @@ class GrouplikeListingElement extends FocusElement {
     if (this.grouplike.items.length) {
       for (const item of this.grouplike.items) {
         const itemElement = new GrouplikeItemElement(item, this.recordStore)
-        for (const evtName of ['download', 'remove', 'select (space)', 'select (enter)', 'queue', 'queue (shuffled)', 'queue (play next)']) {
+        for (const evtName of ['download', 'remove (backspace)', 'remove (x)', 'select (space)', 'select (enter)', 'queue', 'queue (shuffled)', 'queue (play next)']) {
           itemElement.on(evtName, () => this.emit(evtName, item))
         }
         form.addInput(itemElement)
@@ -796,7 +836,9 @@ class GrouplikeItemElement extends Button {
         }
       }
     } else if (telc.isBackspace(keyBuf)) {
-      this.emit('remove')
+      this.emit('remove (backspace)')
+    } else if (telc.isCharacter(keyBuf, 'x')) {
+      this.emit('remove (x)')
     } else if (telc.isSpace(keyBuf)) {
       this.emit('select (space)')
     } else if (telc.isEnter(keyBuf)) {
diff --git a/undo-manager.js b/undo-manager.js
new file mode 100644
index 0000000..4a042ad
--- /dev/null
+++ b/undo-manager.js
@@ -0,0 +1,42 @@
+class UndoManager {
+  constructor() {
+    this.actionStack = []
+    this.undoneStack = []
+  }
+
+  pushAction(action) {
+    this.undoneStack = []
+    this.actionStack.push(action)
+    action.activate()
+  }
+
+  undoLastAction() {
+    if (this.actionStack.length === 0) {
+      return
+    }
+
+    const action = this.actionStack.pop()
+    this.undoneStack.push(action)
+
+    action.undo()
+  }
+
+  redoLastUndoneAction() {
+    if (this.undoneStack.length === 0) {
+      return
+    }
+
+    const action = this.undoneStack.pop()
+    this.actionStack.push(action)
+    action.activate()
+  }
+
+  get safeToPushAction() {
+    // Is it safe to push a new action? That is, since pushing a new action
+    // clears the undone actions stack, will any undone actions be lost?
+
+    return this.undoStack.length === 0
+  }
+}
+
+module.exports = UndoManager