« 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--todo.txt10
-rw-r--r--ui.js151
2 files changed, 99 insertions, 62 deletions
diff --git a/todo.txt b/todo.txt
index 916ec66..dcd847f 100644
--- a/todo.txt
+++ b/todo.txt
@@ -136,4 +136,12 @@ TODO: Entering more than one key "at once" into a text input element will only
 TODO: Pressing space while an "Up (to <group>)" button is selected both
       activates the button and pauses music (because the app detects that the
       space key is pressed). This is definitely wrong (it should do one or the
-      other - I'm not too sure which, yet, but probably the latter).
+      other - I'm not too sure which, yet, but probably the latter). (Done!)
+
+TODO: If a track's file is a symlink, the "From:" label should show where it
+      links to.
+
+TODO: The "Up (to <group>)" and "(This group has no items)" elements are not
+      quite buttons nor grouplike items. They should be more consistent with
+      actual grouplike items (i.e. same behavior and appearance), and should
+      be less buggy. (Done!)
diff --git a/ui.js b/ui.js
index 04d5e3c..5ddba13 100644
--- a/ui.js
+++ b/ui.js
@@ -817,28 +817,24 @@ class GrouplikeListingElement extends Form {
 
     const parent = this.grouplike[parentSymbol]
     if (parent) {
-      const upButton = new Button('Up (to ' + (parent.name || 'unnamed group') + ')')
+      const upButton = new BasicGrouplikeItemElement(`Up (to ${parent.name || 'unnamed group'})`)
       upButton.on('pressed', () => this.loadParentGrouplike())
       form.addInput(upButton)
     }
 
-    let itemElements = []
     if (this.grouplike.items.length) {
-      itemElements = this.grouplike.items.map(item => new GrouplikeItemElement(item, this.recordStore))
-    } else if (!this.grouplike.isTheQueue) {
-      const fakeItem = {
-        fake: true,
-        name: '(This group is empty)',
-        [parentSymbol]: this.grouplike
-      }
-      itemElements = [new GrouplikeItemElement(fakeItem, this.recordStore)]
-    }
+      const itemElements = this.grouplike.items.map(item => {
+        return new InteractiveGrouplikeItemElement(item, this.recordStore)
+      })
 
-    for (const itemElement of itemElements) {
-      for (const evtName of ['download', 'remove', 'mark', 'paste', 'browse', 'queue', 'unqueue', 'menu']) {
-        itemElement.on(evtName, (...data) => this.emit(evtName, itemElement.item, ...data))
+      for (const itemElement of itemElements) {
+        for (const evtName of ['download', 'remove', 'mark', 'paste', 'browse', 'queue', 'unqueue', 'menu']) {
+          itemElement.on(evtName, (...data) => this.emit(evtName, itemElement.item, ...data))
+        }
+        form.addInput(itemElement)
       }
-      form.addInput(itemElement)
+    } else if (!this.grouplike.isTheQueue) {
+      form.addInput(new BasicGrouplikeItemElement('(This group is empty)'))
     }
 
     if (wasSelected) {
@@ -956,7 +952,7 @@ class GrouplikeListingForm extends ListScrollForm {
   }
 
   get firstItemIndex() {
-    return Math.max(0, this.inputs.findIndex(el => el instanceof GrouplikeItemElement))
+    return Math.max(0, this.inputs.findIndex(el => el instanceof InteractiveGrouplikeItemElement))
   }
 
   selectAndShow(item) {
@@ -971,12 +967,11 @@ class GrouplikeListingForm extends ListScrollForm {
   }
 }
 
-class GrouplikeItemElement extends Button {
-  constructor(item, recordStore) {
+class BasicGrouplikeItemElement extends Button {
+  constructor(text) {
     super()
 
-    this.item = item
-    this.recordStore = recordStore
+    this.text = text
   }
 
   fixLayout() {
@@ -1000,60 +995,48 @@ class GrouplikeItemElement extends Button {
 
     writable.write(ansi.moveCursor(this.absTop, this.absLeft))
 
-    if (isGroup(this.item)) {
-      writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT]))
-    }
-
     this.drawX = this.x
     this.writeStatus(writable)
-    writable.write(this.item.name.slice(0, this.w - this.drawX))
-    this.drawX += this.item.name.length
+    writable.write(this.text.slice(0, this.w - this.drawX))
+    this.drawX += this.text.length
     writable.write(' '.repeat(Math.max(0, this.w - this.drawX)))
 
     writable.write(ansi.resetAttributes())
   }
 
   writeStatus(writable) {
-    this.drawX += 3
-
-    const braille = '⠈⠐⠠⠄⠂⠁'
-    const brailleChar = braille[Math.floor(Date.now() / 250) % 6]
-
-    const record = this.recordStore.getRecord(this.item)
-
-    if (this.isMarked) {
-      writable.write('M')
-    } else {
-      writable.write(' ')
-    }
-
-    if (isGroup(this.item)) {
-      writable.write('G')
-    } else if (record.downloading) {
-      writable.write(braille[Math.floor(Date.now() / 250) % 6])
-    } else if (record.playing) {
-      writable.write('\u25B6')
-    } else {
-      writable.write(' ')
-    }
-
-    writable.write(' ')
-  }
-
-  get isMarked() {
-    return this.recordStore.app.editMode && this.recordStore.app.markGrouplike.items.includes(this.item)
+    // Add a couple spaces anyway. This is less than the padding of the tatus
+    // text of elements which represent real playlist items; that's to
+    // distinguish "fake" rows from actual playlist items.
+    writable.write('  ')
+    this.drawX += 2
   }
 
-  get isReal() {
-    return !this.item.fake
+  keyPressed(keyBuf) {
+    // This function is overridden by InteractiveGrouplikeItemElement, but
+    // it's still specified here that only enter counts as an action key.
+    // By default for buttons, the space key also works, but since in this app
+    // space is generally bound to mean "pause" instead of "select", we don't
+    // check if space is pressed here.
+    if (telc.isEnter(keyBuf)) {
+      this.emit('pressed')
+      if (isGroup(this.item)) {
+        this.emit('browse')
+      }
+    }
   }
+}
 
-  get isGroup() {
-    return isGroup(this.item) && this.isReal
+class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement {
+  constructor(item, recordStore) {
+    super(item.name)
+    this.item = item
+    this.recordStore = recordStore
   }
 
-  get isTrack() {
-    return isTrack(this.item) && this.isReal
+  drawTo(writable) {
+    this.text = this.item.name
+    super.drawTo(writable)
   }
 
   keyPressed(keyBuf) {
@@ -1081,7 +1064,7 @@ class GrouplikeItemElement extends Button {
           editMode && this.isReal && {label: this.isMarked ? 'Unmark' : 'Mark', action: () => this.emit('mark')},
           anyMarked && this.isReal && {label: 'Paste (above)', action: () => this.emit('paste', {where: 'above'})},
           anyMarked && this.isReal && {label: 'Paste (below)', action: () => this.emit('paste', {where: 'below'})},
-          anyMarked && !this.isReal && {label: 'Paste', action: () => this.emit('paste')}, // No "above" or "elow" in the label because the fake item will be replaced (it'll disappear, since there'll be an item in the group)
+          anyMarked && !this.isReal && {label: 'Paste', action: () => this.emit('paste')}, // No "above" or "elow" in the label because the "fake" item/row will be replaced (it'll disappear, since there'll be an item in the group)
           this.isReal && {label: 'Play', action: () => this.emit('queue', {where: 'next', play: true})},
           this.isReal && {label: 'Play next', action: () => this.emit('queue', {where: 'next'})},
           this.isReal && {label: 'Play at end', action: () => this.emit('queue', {where: 'end'})},
@@ -1092,6 +1075,52 @@ class GrouplikeItemElement extends Button {
       })
     }
   }
+
+  writeStatus(writable) {
+    if (isGroup(this.item)) {
+      // The ANSI attributes here will apply to the rest of the line, too.
+      // (We don't reset the active attributes until after drawing the rest of
+      // the line.)
+      writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT]))
+    }
+
+    this.drawX += 3
+
+    const braille = '⠈⠐⠠⠄⠂⠁'
+    const brailleChar = braille[Math.floor(Date.now() / 250) % 6]
+
+    const record = this.recordStore.getRecord(this.item)
+
+    if (this.isMarked) {
+      writable.write('M')
+    } else {
+      writable.write(' ')
+    }
+
+    if (isGroup(this.item)) {
+      writable.write('G')
+    } else if (record.downloading) {
+      writable.write(braille[Math.floor(Date.now() / 250) % 6])
+    } else if (record.playing) {
+      writable.write('\u25B6')
+    } else {
+      writable.write(' ')
+    }
+
+    writable.write(' ')
+  }
+
+  get isMarked() {
+    return this.recordStore.app.editMode && this.recordStore.app.markGrouplike.items.includes(this.item)
+  }
+
+  get isGroup() {
+    return isGroup(this.item)
+  }
+
+  get isTrack() {
+    return isTrack(this.item)
+  }
 }
 
 class ListingJumpElement extends Form {