« 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--ui.js91
2 files changed, 89 insertions, 3 deletions
diff --git a/README.md b/README.md
index 5c95677..d66a65b 100644
--- a/README.md
+++ b/README.md
@@ -61,3 +61,4 @@ You're also welcome to share any ideas, suggestions, and questions through there
 * **In a context menu:**
   * All the usual controls (up, down, tab, space, enter) work
   * <kbd>Backspace</kbd> or <kbd>Escape</kbd> - close the menu without taking any action
+  * Type the beginning of the name of an option, like its first letter, to quickly jump to it
diff --git a/ui.js b/ui.js
index d9b652d..de2226a 100644
--- a/ui.js
+++ b/ui.js
@@ -273,7 +273,7 @@ class AppElement extends FocusElement {
           `(${item.name ? `"${item.name}"` : 'Unnamed'} -` +
           ` ${item.items.length} item${item.items.length === 1 ? '' : 's'}` +
           `, ${countTotalItems(item)} total` +
-          ')'},
+          ')', keyboardIdentifier: item.name},
 
         // The actual controls!
         {divider: true},
@@ -1451,6 +1451,7 @@ class InlineListPickerElement extends FocusElement {
     this.labelText = labelText
     this.options = options
     this.curIndex = 0
+    this.keyboardIdentifier = this.labelText
   }
 
   fixLayout() {
@@ -2210,6 +2211,8 @@ class ContextMenu extends FocusElement {
     this.form = new ListScrollForm()
     this.pane.addChild(this.form)
 
+    this.keyboardSelector = new KeyboardSelector(this.form)
+
     this.visible = false
   }
 
@@ -2238,6 +2241,7 @@ class ContextMenu extends FocusElement {
         this.form.addInput(element)
       } else {
         const button = new Button(item.label)
+        button.keyboardIdentifier = item.keyboardIdentifier || item.label
         if (item.action) {
           button.on('pressed', () => {
             this.close()
@@ -2257,9 +2261,20 @@ class ContextMenu extends FocusElement {
     if (telc.isEscape(keyBuf) || telc.isBackspace(keyBuf)) {
       this.close()
       return false
-    } else {
-      super.keyPressed(keyBuf)
+    } else if (this.keyboardSelector.keyPressed(keyBuf)) {
+      return false
+
+      const key = String.fromCharCode(keyBuf[0]).toLowerCase()
+      for (const input of this.form.inputs) {
+        if (input.selectable && input.menuKeyShortcut) {
+          if (input.menuKeyShortcut.toLowerCase() === key) {
+            this.form.selectInput(input)
+            return false
+          }
+        }
+      }
     }
+    super.keyPressed(keyBuf)
   }
 
   unselected() {
@@ -2349,4 +2364,74 @@ class HorizontalRule extends FocusElement {
   }
 }
 
+class KeyboardSelector {
+  // Class used to select things when you type out their name. Specify strings
+  // used to access each element of a form in the keyboardIdentifier property.
+  // (Elements without a keyboardIdentifier, or which are !selectable, will be
+  // skipped.)
+
+  constructor(form) {
+    this.value = ''
+    this.form = form
+  }
+
+  keyPressed(keyBuf) {
+    // Don't do anything if the input isn't a single keyboard character.
+    if (keyBuf.length !== 1 || keyBuf[0] <= 31 || keyBuf[0] >= 127) {
+      return
+    }
+
+    // First see if a result is found when we append the typed character to our
+    // recorded input.
+    const char = keyBuf.toString()
+    this.value += char
+    if (!KeyboardSelector.find(this.value, this.form)) {
+      // If we don't find a result, replace our recorded input with the single
+      // character entered, then do a search. Start from the input after the
+      // current-selected one, so that we don't just end up re-selecting the
+      // element that was selected before, if there's another option that would
+      // match this key ahead. (This is so that you can easily type a string or
+      // character over and over to navigate through options that all start
+      // with the same string/character.)
+      this.value = char
+      return KeyboardSelector.find(this.value, this.form, this.form.curIndex + 1)
+    }
+    return true
+  }
+
+  static find(text, form, fromIndex = form.curIndex) {
+    // Most of this code is just stolen from AppElement's code for handling
+    // input from JumpElement!
+
+    const lower = text.toLowerCase()
+    const getName = inp => inp.keyboardIdentifier ? inp.keyboardIdentifier.toLowerCase().trim() : ''
+
+    const testStartsWith = inp => getName(inp).startsWith(lower)
+
+    const searchPastCurrentIndex = test => {
+      const start = fromIndex
+      const match = form.inputs.slice(start).findIndex(test)
+      if (match === -1) {
+        return -1
+      } else {
+        return start + match
+      }
+    }
+
+    const allIndexes = [
+      searchPastCurrentIndex(testStartsWith),
+      form.inputs.findIndex(testStartsWith),
+    ]
+
+    const matchedIndex = allIndexes.find(value => value >= 0)
+
+    if (typeof matchedIndex !== 'undefined') {
+      form.selectInput(form.inputs[matchedIndex])
+      return true
+    } else {
+      return false
+    }
+  }
+}
+
 module.exports.AppElement = AppElement