« 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--README.md1
-rw-r--r--todo.txt7
m---------tui-lib0
-rw-r--r--ui.js84
4 files changed, 91 insertions, 1 deletions
diff --git a/README.md b/README.md
index 7f6a10c..f245848 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ playlist.json file (usually generated by http-music or downloaded from online).
 * <kbd>Escape</kbd> - stop playing the current track
 * <kbd>Right</kbd> - seek ahead
 * <kbd>Left</kbd> - seek back
+* <kbd><kbd>Ctrl</kbd>+<kbd>O</kbd></kbd> - jump to a track or group by entering (part of) its name
 * <kbd><kbd>Ctrl</kbd>+<kbd>O</kbd></kbd> - open a playlist from a source (like a /path/to/a/folder or a YouTube playlist URL) (you can also just pass this source to `mtui`)
 * <kbd>t</kbd> and <kbd>T</kbd> (shift+T) - switch between playlist tabs
 * <kbd><kbd>Ctrl</kbd>+<kbd>T</kbd></kbd> - open the current playlist in a new tab (so, clone the current tab)
diff --git a/todo.txt b/todo.txt
index 25c8369..bde9a89 100644
--- a/todo.txt
+++ b/todo.txt
@@ -117,3 +117,10 @@ TODO: A "remove from queue" option for tracks and groups, which removes them
 TODO: After the end of a shuffled queue, the next song from the group of the
       last track is played (and so on, until the group is empty). This seems
       definitely wrong.
+
+TODO: Show a preview of where "Jump to" will go while typing.
+
+TODO: Entering more than one key "at once" into a text input element will only
+      move the cursor right by one character, not by the length of the inputted
+      text. (This is an issue when pasting or spamming the keyboard.) Should be
+      fixed in tui-lib.
diff --git a/tui-lib b/tui-lib
-Subproject 68d5c75dce1c950468d07241261fb9f8c8e3d39
+Subproject e2f830680c1a6e9f28ad305b105caf5cdf092e6
diff --git a/ui.js b/ui.js
index 3da7f5c..1d8d2da 100644
--- a/ui.js
+++ b/ui.js
@@ -708,6 +708,16 @@ class GrouplikeListingElement extends Form {
       }
     })
 
+    this.jumpElement = new ListingJumpElement()
+    this.addInput(this.jumpElement)
+    this.jumpElement.visible = false
+
+    this.jumpElement.on('cancel', () => {
+      this.hideJumpElement()
+    })
+
+    this.jumpElement.on('value', value => this.handleJumpValue(value))
+
     this.pathElement = new PathElement()
     this.addInput(this.pathElement)
 
@@ -727,9 +737,15 @@ class GrouplikeListingElement extends Form {
     this.form.y = this.commentLabel.bottom
     this.form.h -= this.commentLabel.h
     this.form.h -= 1 // For the path element
+    if (this.jumpElement.visible) this.form.h -= 1
+
+    this.form.fixLayout() // Respond to being resized
 
     this.pathElement.y = this.contentH - 1
     this.pathElement.w = this.contentW
+
+    this.jumpElement.y = this.pathElement.y - 1
+    this.jumpElement.w = this.contentW
   }
 
   selected() {
@@ -744,6 +760,8 @@ class GrouplikeListingElement extends Form {
   keyPressed(keyBuf) {
     if (telc.isBackspace(keyBuf)) {
       this.loadParentGrouplike()
+    } else if (telc.isCharacter(keyBuf, '/') || keyBuf[0] === 6) { // '/', ctrl-F
+      this.showJumpElement()
     } else {
       return super.keyPressed(keyBuf)
     }
@@ -837,6 +855,40 @@ class GrouplikeListingElement extends Form {
     this.form.selectAndShow(item)
   }
 
+  handleJumpValue(value) {
+    // Don't perform the search if the user didn't enter anything.
+    if (value.length) {
+      const lower = value.toLowerCase()
+      const getName = inp => (inp.item && inp.item.name) ? inp.item.name.toLowerCase().trim() : ''
+      // TODO: Search past the current index, for repeated searches?
+      const startsIndex = this.form.inputs.findIndex(inp => getName(inp).startsWith(lower))
+      const includesIndex = this.form.inputs.findIndex(inp => getName(inp).includes(lower))
+      const matchedIndex = startsIndex >= 0 ? startsIndex : includesIndex
+
+      if (matchedIndex >= 0) {
+        this.form.curIndex = matchedIndex
+        this.form.scrollSelectedElementIntoView()
+      } else {
+        // TODO: Feedback that the search failed.. right now we just close the
+        // jump-to menu, which might not be right.
+      }
+    }
+
+    this.hideJumpElement()
+  }
+
+  showJumpElement() {
+    this.jumpElement.visible = true
+    this.root.select(this.jumpElement)
+    this.fixLayout()
+  }
+
+  hideJumpElement() {
+    this.jumpElement.visible = false
+    this.root.select(this)
+    this.fixLayout()
+  }
+
   get tabberLabel() {
     if (this.grouplike) {
       return this.grouplike.name || 'Unnamed group'
@@ -872,7 +924,6 @@ class GrouplikeListingForm extends ListScrollForm {
   }
 
   selectAndShow(item) {
-    // TODO: Make sure this is still working.
     const index = this.inputs.findIndex(inp => inp.item === item)
     if (index >= 0) {
       this.curIndex = index
@@ -998,6 +1049,37 @@ class GrouplikeItemElement extends Button {
   }
 }
 
+class ListingJumpElement extends Form {
+  constructor() {
+    super()
+
+    this.label = new Label('Jump to: ')
+    this.addChild(this.label)
+
+    this.input = new TextInput()
+    this.addInput(this.input)
+
+    this.input.on('value', value => {
+      this.emit('value', value)
+    })
+
+    this.input.on('cancel', () => {
+      this.emit('cancel')
+    })
+  }
+
+  selected() {
+    this.input.value = ''
+    this.input.keepCursorInRange()
+    this.root.select(this.input)
+  }
+
+  fixLayout() {
+    this.input.x = this.label.right
+    this.input.w = this.contentW - this.input.x
+  }
+}
+
 class PathElement extends ListScrollForm {
   constructor() {
     super('horizontal')