« get me outta code hell

Add text/notes editor, using tui-text-editor - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/ui.js
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-10-23 12:20:27 -0300
committerFlorrie <towerofnix@gmail.com>2019-10-23 12:20:27 -0300
commit147172fe720f8af8a219ba384c0407ca751e0321 (patch)
tree85b97ccb8ea210392493806ebf6d35e41615e76b /ui.js
parent1d1483a9dc18d7acf4267727893a281b99645c02 (diff)
Add text/notes editor, using tui-text-editor
:D!
Diffstat (limited to 'ui.js')
-rw-r--r--ui.js169
1 files changed, 160 insertions, 9 deletions
diff --git a/ui.js b/ui.js
index 03337f5..cfb3e87 100644
--- a/ui.js
+++ b/ui.js
@@ -15,9 +15,12 @@ const {
   cloneGrouplike,
   countTotalTracks,
   flattenGrouplike,
+  getCorrespondingFileForItem,
   getItemPath,
   getNameWithoutTrackNumber,
   isGroup,
+  isOpenable,
+  isPlayable,
   isTrack,
   parentSymbol,
   reverseOrderOfGroups,
@@ -47,10 +50,15 @@ const {
   }
 } = require('tui-lib')
 
-const open = require('open')
+const TuiTextEditor = require('tui-text-editor')
 
-const isPlayable = item => isGroup(item) || isTrack(item)
-const isOpenable = item => !!(item && item.url)
+const { promisify } = require('util')
+const fs = require('fs')
+const open = require('open')
+const path = require('path')
+const url = require('url')
+const readFile = promisify(fs.readFile)
+const writeFile = promisify(fs.writeFile)
 
 const input = {}
 
@@ -101,6 +109,10 @@ const keyBindings = [
   ['isActOnPlayer', [0x1b, 'a']],
   ['isActOnPlayer', [0x1b, '!']],
 
+  ['isSaveTextEditor', [0x13]], // ^S
+  ['isDeselectTextEditor', [0x18]], // ^X
+  ['isDeselectTextEditor', telc.isEscape],
+
   // Number pad
   ['isUp', '8'],
   ['isDown', '2'],
@@ -200,6 +212,15 @@ class AppElement extends FocusElement {
     this.queuePane = new Pane()
     this.addChild(this.queuePane)
 
+    this.textInfoPane = new Pane()
+    this.addChild(this.textInfoPane)
+
+    this.textEditor = new NotesTextEditor()
+    this.textInfoPane.addChild(this.textEditor)
+    this.textInfoPane.visible = false
+
+    this.textEditor.on('deselect', () => this.root.select(this.tabber))
+
     if (!this.config.showTabberPane) {
       this.tabberPane.visible = false
     }
@@ -677,6 +698,20 @@ class AppElement extends FocusElement {
 
     grouplikeListing.pathElement.on('select', item => this.reveal(item))
     grouplikeListing.on('menu', (item, el) => this.showMenuForItemElement(el, grouplikeListing))
+
+    grouplikeListing.on('selected', async item => {
+      const status = await this.textEditor.openItem(item, {
+        doubleCheckItem: () => item === grouplikeListing.currentItem
+      })
+
+      if (status === true) {
+        this.textInfoPane.visible = true
+        this.fixLayout()
+      } else if (status === false) {
+        this.textInfoPane.visible = false
+        this.fixLayout()
+      }
+    })
   }
 
   showContextMenu(opts) {
@@ -800,6 +835,10 @@ class AppElement extends FocusElement {
   openSpecialOrThroughSystem(item) {
     if (item.url.endsWith('.json')) {
       return this.handlePlaylistSource(item.url, true)
+    } else if (item.url.endsWith('.txt')) {
+      if (this.textInfoPane.visible) {
+        this.root.select(this.textEditor)
+      }
     } else {
       return this.openThroughSystem(item)
     }
@@ -921,11 +960,9 @@ class AppElement extends FocusElement {
         canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => this.processMetadata(item, false)},
         canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => this.processMetadata(item, true)},
         canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => this.processMetadata(item, true)},
-        canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
-        {divider: true},
-
         isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)},
         isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)},
+        canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
         {divider: true},
 
         item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()}
@@ -999,6 +1036,7 @@ class AppElement extends FocusElement {
       await this.backend.stopPlayingAll()
     }
 
+    await this.textEditor.save()
     this.emit('quitRequested')
   }
 
@@ -1057,15 +1095,37 @@ class AppElement extends FocusElement {
       this.alignPartyLabel()
     }
 
+    const leftWidth = Math.max(Math.floor(0.8 * this.contentW), this.contentW - 80)
+
+    if (this.textInfoPane.visible) {
+      this.textInfoPane.w = leftWidth
+      if (this.textEditor.isSelected) {
+        this.textInfoPane.h = 8
+      } else {
+        this.textEditor.w = this.textInfoPane.contentW
+        this.textInfoPane.h = Math.min(8, this.textEditor.getOptimalHeight() + 2)
+      }
+
+      this.textEditor.fillParent()
+      this.textEditor.fixLayout()
+    }
+
     if (this.tabberPane.visible) {
-      this.tabberPane.w = Math.max(Math.floor(0.8 * this.contentW), this.contentW - 80)
+      this.tabberPane.w = leftWidth
       this.tabberPane.y = bottomY
       this.tabberPane.h = topY - this.tabberPane.y
+      if (this.textInfoPane.visible) {
+        this.tabberPane.h -= this.textInfoPane.h
+        this.textInfoPane.y = this.tabberPane.bottom
+      }
       this.queuePane.x = this.tabberPane.right
       this.queuePane.w = this.contentW - this.tabberPane.right
     } else {
       this.queuePane.x = 0
       this.queuePane.w = this.contentW
+      if (this.textInfoPane.visible) {
+        this.textInfoPane.y = bottomY
+      }
     }
 
     this.queuePane.y = bottomY
@@ -1440,9 +1500,10 @@ class GrouplikeListingElement extends Form {
     this.form = this.getNewForm()
     this.addInput(this.form)
 
-    this.form.on('selected input', input => {
+    this.form.on('selected', input => {
       if (input && this.pathElement) {
         this.pathElement.showItem(input.item)
+        this.emit('selected', input.item)
       }
     })
 
@@ -1490,6 +1551,7 @@ class GrouplikeListingElement extends Form {
   selected() {
     this.curIndex = 0
     this.root.select(this.form)
+    this.emit('selected', this.currentItem)
   }
 
   clicked(button) {
@@ -1782,7 +1844,7 @@ class GrouplikeListingForm extends ListScrollForm {
 
   set curIndex(newIndex) {
     this.setDep('curIndex', newIndex)
-    this.emit('selected input', this.inputs[this.curIndex])
+    this.emit('selected', this.inputs[this.curIndex])
     return newIndex
   }
 
@@ -3884,4 +3946,93 @@ class PartyBanner extends DisplayElement {
   }
 }
 
+class NotesTextEditor extends TuiTextEditor {
+  constructor() {
+    super()
+
+    this.openedItem = null
+  }
+
+  keyPressed(keyBuf) {
+    if (input.isDeselectTextEditor(keyBuf)) {
+      this.emit('deselect')
+      return false
+    } else if (input.isSaveTextEditor(keyBuf)) {
+      this.saveManually()
+      return false
+    } else {
+      return super.keyPressed(keyBuf)
+    }
+  }
+
+  async openItem(item, {doubleCheckItem}) {
+    if (this.hasBeenEdited) {
+      // Save in the background.
+      this.save()
+    }
+
+    const textFile = getCorrespondingFileForItem(item, '.txt')
+    if (!textFile) {
+      this.openedItem = null
+      return false
+    }
+
+    if (textFile === this.openedItem) {
+      // This file is already open - don't do anything.
+      return null
+    }
+
+    let filePath
+    try {
+      filePath = url.fileURLToPath(new URL(textFile.url))
+    } catch (error) {
+      this.openedItem = null
+      return false
+    }
+
+    let buffer
+    try {
+      buffer = await readFile(filePath)
+    } catch (error) {
+      this.openedItem = null
+      return false
+    }
+
+    if (!doubleCheckItem(item)) {
+      return null
+    }
+
+    this.openedItem = textFile
+    this.openedPath = filePath
+    this.clearSourceAndLoadText(buffer.toString())
+    return true
+  }
+
+  async saveManually() {
+    if (!this.openedItem || !this.openedPath) {
+      return
+    }
+
+    const item = this.openedItem
+
+    if (await this.save()) {
+      if (item === this.openedItem) {
+        this.showStatusMessage('Saved manually.')
+      }
+    }
+  }
+
+  async save() {
+    const text = this.getSourceText()
+    try {
+      await writeFile(this.openedPath, text)
+      this.clearEditStatus()
+      return true
+    } catch (error) {
+      this.showStatusMessage(`Failed to save (${path.basename(this.openedPath)}: ${error.code}).`)
+      return false
+    }
+  }
+}
+
 module.exports = AppElement