« 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--.eslintrc24
-rw-r--r--backend.js50
-rw-r--r--client.js36
-rw-r--r--combine-album.js15
-rw-r--r--crawlers.js46
-rw-r--r--downloaders.js94
-rw-r--r--general-util.js48
-rw-r--r--guess.js24
-rwxr-xr-xindex.js43
-rw-r--r--metadata-readers.js23
-rw-r--r--package-lock.json1955
-rw-r--r--package.json11
-rw-r--r--players.js56
-rw-r--r--playlist-utils.js137
-rw-r--r--record-store.js2
-rw-r--r--smart-playlist.js8
-rw-r--r--socat.js18
-rw-r--r--telnet.js23
-rw-r--r--todo.txt6
-rw-r--r--ui.js131
-rw-r--r--undo-manager.js4
21 files changed, 2268 insertions, 486 deletions
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..f742bb8
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,24 @@
+{
+  "env": {
+    "es2021": true,
+    "node": true
+  },
+  "extends": "eslint:recommended",
+  "parserOptions": {
+    "ecmaVersion": "latest",
+    "sourceType": "module"
+  },
+  "rules": {
+    "indent": ["off"],
+    "no-constant-condition": ["error", {
+      "checkLoops": false
+    }],
+    "no-empty": ["error", {
+      "allowEmptyCatch": true
+    }],
+    "no-unused-vars": ["error", {
+      "argsIgnorePattern": "^_",
+      "destructuredArrayIgnorePattern": "^"
+    }]
+  }
+}
diff --git a/backend.js b/backend.js
index 59c4a48..4142026 100644
--- a/backend.js
+++ b/backend.js
@@ -3,31 +3,27 @@
 
 'use strict'
 
-const { getDownloaderFor } = require('./downloaders')
-const { getMetadataReaderFor } = require('./metadata-readers')
-const { getPlayer } = require('./players')
-const RecordStore = require('./record-store')
-const os = require('os')
+import {readFile, writeFile} from 'node:fs/promises'
+import EventEmitter from 'node:events'
+import os from 'node:os'
 
-const {
+import {getDownloaderFor} from './downloaders.js'
+import {getMetadataReaderFor} from './metadata-readers.js'
+import {getPlayer} from './players.js'
+import RecordStore from './record-store.js'
+
+import {
   getTimeStringsFromSec,
   shuffleArray,
-  throttlePromise
-} = require('./general-util')
+  throttlePromise,
+} from './general-util.js'
 
-const {
+import {
   isGroup,
   isTrack,
   flattenGrouplike,
-  getItemPathString,
-  parentSymbol
-} = require('./playlist-utils')
-
-const { promisify } = require('util')
-const EventEmitter = require('events')
-const fs = require('fs')
-const writeFile = promisify(fs.writeFile)
-const readFile = promisify(fs.readFile)
+  parentSymbol,
+} from './playlist-utils.js'
 
 async function download(item, record) {
   if (isGroup(item)) {
@@ -206,20 +202,6 @@ class QueuePlayer extends EventEmitter {
 
     const distributeSize = distributeEnd - distributeStart
 
-    const queueItem = (item, insertIndex) => {
-      if (items.includes(item)) {
-        /*
-        if (!movePlayingTrack && item === this.playingTrack) {
-          return
-        }
-        */
-        items.splice(items.indexOf(item), 1)
-      } else {
-        offset++
-      }
-      items.splice(insertIndex, 0, item)
-    }
-
     if (how === 'evenly') {
       let offset = 0
       for (const item of newTracks) {
@@ -647,7 +629,7 @@ class QueuePlayer extends EventEmitter {
   }
 }
 
-class Backend extends EventEmitter {
+export default class Backend extends EventEmitter {
   constructor({
     playerName = null,
     playerOptions = []
@@ -830,5 +812,3 @@ class Backend extends EventEmitter {
     return download(item, this.getRecordFor(item))
   }
 }
-
-module.exports = Backend
diff --git a/client.js b/client.js
index ec1ab60..0af45f6 100644
--- a/client.js
+++ b/client.js
@@ -1,22 +1,18 @@
 // Generic code for setting up mtui and the UI for any command line client.
 
-'use strict'
-
-const AppElement = require('./ui')
-const processSmartPlaylist = require('./smart-playlist')
-
-const {
-  ui: {
-    Root
-  },
-  util: {
-    ansi,
-    Flushable,
-    TelnetInterfacer
-  }
-} = require('tui-lib')
+import AppElement from './ui.js'
+
+import {Root} from 'tui-lib/ui/primitives'
 
-const setupClient = async ({backend, writable, interfacer, appConfig}) => {
+import {Flushable} from 'tui-lib/util/interfaces'
+import * as ansi from 'tui-lib/util/ansi'
+
+export default async function setupClient({
+  backend,
+  writable,
+  screenInterface,
+  appConfig,
+}) {
   const cleanTerminal = () => {
     writable.write(ansi.cleanCursor())
     writable.write(ansi.disableAlternateScreen())
@@ -30,10 +26,10 @@ const setupClient = async ({backend, writable, interfacer, appConfig}) => {
   dirtyTerminal()
 
   const flushable = new Flushable(writable, true)
-  const root = new Root(interfacer, flushable)
+  const root = new Root(screenInterface, flushable)
   root.on('rendered', () => flushable.flush())
 
-  const size = await interfacer.getScreenSize()
+  const size = await screenInterface.getScreenSize()
   root.w = size.width
   root.h = size.height
   root.fixAllLayout()
@@ -42,7 +38,7 @@ const setupClient = async ({backend, writable, interfacer, appConfig}) => {
   flushable.write(ansi.clearScreen())
   flushable.flush()
 
-  interfacer.on('resize', newSize => {
+  screenInterface.on('resize', newSize => {
     root.w = newSize.width
     root.h = newSize.height
     flushable.resizeScreen(newSize)
@@ -69,5 +65,3 @@ const setupClient = async ({backend, writable, interfacer, appConfig}) => {
 
   return {appElement, cleanTerminal, dirtyTerminal, flushable, root}
 }
-
-module.exports = setupClient
diff --git a/combine-album.js b/combine-album.js
index 946c4c1..3b57b6c 100644
--- a/combine-album.js
+++ b/combine-album.js
@@ -1,12 +1,13 @@
 'use strict'
 
-// too lazy to use import syntax :)
-const { readdir, readFile, stat, writeFile } = require('fs/promises')
-const { spawn } = require('child_process')
-const { getTimeStringsFromSec, parseOptions, promisifyProcess } = require('./general-util')
-const { musicExtensions } = require('./crawlers')
-const path = require('path')
-const shellescape = require('shell-escape')
+import {readdir, readFile, stat, writeFile} from 'node:fs/promises'
+import {spawn} from 'node:child_process'
+import path from 'node:path'
+
+import shellescape from 'shell-escape'
+
+import {musicExtensions} from './crawlers.js'
+import {getTimeStringsFromSec, parseOptions, promisifyProcess} from './general-util.js'
 
 async function timestamps(files) {
   const tsData = []
diff --git a/crawlers.js b/crawlers.js
index 6af615d..b2f13fd 100644
--- a/crawlers.js
+++ b/crawlers.js
@@ -1,25 +1,21 @@
-const fs = require('fs')
-const path = require('path')
-const expandHomeDir = require('expand-home-dir')
-const fetch = require('node-fetch')
-const url = require('url')
-const { downloadPlaylistFromOptionValue, promisifyProcess } = require('./general-util')
-const { spawn } = require('child_process')
-const { orderBy } = require('natural-orderby')
-
-const { promisify } = require('util')
-const readDir = promisify(fs.readdir)
-const stat = promisify(fs.stat)
-
-const musicExtensions = [
+import {spawn} from 'node:child_process'
+import {readdir, stat} from 'node:fs/promises'
+import url from 'node:url'
+import path from 'node:path'
+
+import {orderBy} from 'natural-orderby'
+import expandHomeDir from 'expand-home-dir'
+// import fetch from 'node-fetch'
+
+import {downloadPlaylistFromOptionValue, promisifyProcess} from './general-util.js'
+
+export const musicExtensions = [
   'ogg', 'oga',
   'wav', 'mp3', 'm4a', 'aac', 'flac', 'opus',
   'mp4', 'mov', 'mkv',
   'mod'
 ]
 
-module.exports.musicExtensions = musicExtensions
-
 // Each value is a function with these additional properties:
 // * crawlerName: The name of the crawler, such as "crawl-http". Used by
 //   getCrawlerByName.
@@ -30,7 +26,7 @@ module.exports.musicExtensions = musicExtensions
 const allCrawlers = {}
 
 /* TODO: Removed cheerio, so crawl-http no longer works.
-function crawlHTTP(absURL, opts = {}, internals = {}) {
+export function crawlHTTP(absURL, opts = {}, internals = {}) {
   // Recursively crawls a given URL, following every link to a deeper path and
   // recording all links in a tree (in the same format playlists use). Makes
   // multiple attempts to download failed paths.
@@ -251,7 +247,7 @@ function crawlLocal(dirPath, extensions = musicExtensions, isTop = true) {
     dirPath = expandHomeDir(dirPath)
   }
 
-  return readDir(dirPath).then(items => {
+  return readdir(dirPath).then(items => {
     items = orderBy(items)
 
     return Promise.all(items.map(item => {
@@ -278,7 +274,7 @@ function crawlLocal(dirPath, extensions = musicExtensions, isTop = true) {
             return {name: item, url: itemURL}
           }
         }
-      }, statErr => null)
+      }, _statErr => null)
     }))
   }, err => {
     if (err.code === 'ENOENT') {
@@ -325,7 +321,7 @@ crawlLocal.isAppropriateForArg = function(arg) {
 
 allCrawlers.crawlLocal = crawlLocal
 
-async function crawlYouTube(url) {
+export async function crawlYouTube(url) {
   const ytdl = spawn('youtube-dl', [
     '-j', // Output as JSON
     '--flat-playlist',
@@ -385,7 +381,7 @@ crawlYouTube.isAppropriateForArg = function(arg) {
 
 allCrawlers.crawlYouTube = crawlYouTube
 
-async function openFile(input) {
+export async function openFile(input) {
   return JSON.parse(await downloadPlaylistFromOptionValue(input))
 }
 
@@ -398,14 +394,10 @@ openFile.isAppropriateForArg = function(arg) {
 
 allCrawlers.openFile = openFile
 
-// Actual module.exports stuff:
-
-Object.assign(module.exports, allCrawlers)
-
-module.exports.getCrawlerByName = function(name) {
+export function getCrawlerByName(name) {
   return Object.values(allCrawlers).find(fn => fn.crawlerName === name)
 }
 
-module.exports.getAllCrawlersForArg = function(arg) {
+export function getAllCrawlersForArg(arg) {
   return Object.values(allCrawlers).filter(fn => fn.isAppropriateForArg(arg))
 }
diff --git a/downloaders.js b/downloaders.js
index 941c805..9e7c786 100644
--- a/downloaders.js
+++ b/downloaders.js
@@ -1,25 +1,21 @@
-const { promisifyProcess } = require('./general-util')
-const { promisify } = require('util')
-const { spawn } = require('child_process')
-const { URL } = require('url')
-const mkdirp = promisify(require('mkdirp'))
-const fs = require('fs')
-const fetch = require('node-fetch')
-const tempy = require('tempy')
-const os = require('os')
-const path = require('path')
-const sanitize = require('sanitize-filename')
-
-const writeFile = promisify(fs.writeFile)
-const rename = promisify(fs.rename)
-const stat = promisify(fs.stat)
-const readdir = promisify(fs.readdir)
-const symlink = promisify(fs.symlink)
+import {spawn} from 'node:child_process'
+import {createReadStream, createWriteStream} from 'node:fs'
+import {readdir, rename, stat, symlink, writeFile} from 'node:fs/promises'
+import os from 'node:os'
+import path from 'node:path'
+import url from 'node:url'
+
+import {mkdirp} from 'mkdirp'
+import fetch from 'node-fetch'
+import sanitize from 'sanitize-filename'
+import tempy from 'tempy'
+
+import {promisifyProcess} from './general-util.js'
 
 const copyFile = (source, target) => {
   // Stolen from https://stackoverflow.com/a/30405105/4633828
-  const rd = fs.createReadStream(source)
-  const wr = fs.createWriteStream(target)
+  const rd = createReadStream(source)
+  const wr = createWriteStream(target)
   return new Promise((resolve, reject) => {
     rd.on('error', reject)
     wr.on('error', reject)
@@ -32,7 +28,7 @@ const copyFile = (source, target) => {
   })
 }
 
-// const disableBackResolving = arg => arg.split('/').map(str => str.replace(/^\../, '_..')).join('/')
+export const rootCacheDir = path.join(os.homedir(), '.mtui', 'downloads')
 
 const cachify = (identifier, keyFunction, baseFunction) => {
   return async arg => {
@@ -43,7 +39,7 @@ const cachify = (identifier, keyFunction, baseFunction) => {
 
     // Determine where the final file will end up. This is just a directory -
     // the file's own name is determined by the downloader.
-    const cacheDir = downloaders.rootCacheDir + '/' + identifier
+    const cacheDir = rootCacheDir + '/' + identifier
     const finalDirectory = cacheDir + '/' + sanitize(keyFunction(arg))
 
     // Check if that directory only exists. If it does, return the file in it,
@@ -102,15 +98,16 @@ const removeFileProtocol = arg => {
   }
 }
 
-const downloaders = {
-  extension: 'mp3', // Generally target file extension, used by youtube-dl
+// Generally target file extension, used by youtube-dl
+export const extension = 'mp3'
 
-  rootCacheDir: os.homedir() + '/.mtui/downloads',
+const downloaders = {}
 
-  http: cachify('http',
+downloaders.http =
+  cachify('http',
     arg => {
-      const url = new URL(arg)
-      return url.hostname + url.pathname
+      const {hostname, pathname} = new url.URL(arg)
+      return hostname + pathname
     },
     arg => {
       const out = (
@@ -121,9 +118,10 @@ const downloaders = {
         .then(response => response.buffer())
         .then(buffer => writeFile(out, buffer))
         .then(() => out)
-    }),
+    })
 
-  youtubedl: cachify('youtubedl',
+downloaders.youtubedl =
+  cachify('youtubedl',
     arg => (arg.match(/watch\?v=(.*)/) || ['', arg])[1],
     arg => {
       const outDir = tempy.directory()
@@ -133,7 +131,7 @@ const downloaders = {
         '--quiet',
         '--no-warnings',
         '--extract-audio',
-        '--audio-format', downloaders.extension,
+        '--audio-format', extension,
         '--output', outFile,
         arg
       ]
@@ -141,9 +139,10 @@ const downloaders = {
       return promisifyProcess(spawn('youtube-dl', opts))
         .then(() => readdir(outDir))
         .then(files => outDir + '/' + files[0])
-    }),
+    })
 
-  local: cachify('local',
+downloaders.local =
+  cachify('local',
     arg => arg,
     arg => {
       // Usually we'd just return the given argument in a local
@@ -171,9 +170,10 @@ const downloaders = {
 
       return copyFile(arg, out)
         .then(() => out)
-    }),
+    })
 
-  locallink: cachify('locallink',
+downloaders.locallink =
+  cachify('locallink',
     arg => arg,
     arg => {
       // Like the local downloader, but creates a symbolic link to the argument.
@@ -184,22 +184,22 @@ const downloaders = {
 
       return symlink(path.resolve(arg), out)
         .then(() => out)
-    }),
+    })
 
-  echo: arg => arg,
+downloaders.echo =
+  arg => arg
 
-  getDownloaderFor: arg => {
-    if (arg.startsWith('http://') || arg.startsWith('https://')) {
-      if (arg.includes('youtube.com')) {
-        return downloaders.youtubedl
-      } else {
-        return downloaders.http
-      }
+export default downloaders
+
+export function getDownloaderFor(arg) {
+  if (arg.startsWith('http://') || arg.startsWith('https://')) {
+    if (arg.includes('youtube.com')) {
+      return downloaders.youtubedl
     } else {
-      // return downloaders.local
-      return downloaders.locallink
+      return downloaders.http
     }
+  } else {
+    // return downloaders.local
+    return downloaders.locallink
   }
 }
-
-module.exports = downloaders
diff --git a/general-util.js b/general-util.js
index aba1541..bb0574a 100644
--- a/general-util.js
+++ b/general-util.js
@@ -1,13 +1,11 @@
-const { spawn } = require('child_process')
-const { promisify } = require('util')
-const fetch = require('node-fetch')
-const fs = require('fs')
-const npmCommandExists = require('command-exists')
-const url = require('url')
+import {spawn} from 'node:child_process'
+import {readFile} from 'node:fs/promises'
+import {fileURLToPath, URL} from 'node:url'
 
-const readFile = promisify(fs.readFile)
+import npmCommandExists from 'command-exists'
+import fetch from 'node-fetch'
 
-module.exports.promisifyProcess = function(proc, showLogging = true) {
+export function promisifyProcess(proc, showLogging = true) {
   // Takes a process (from the child_process module) and returns a promise
   // that resolves when the process exits (or rejects, if the exit code is
   // non-zero).
@@ -28,7 +26,7 @@ module.exports.promisifyProcess = function(proc, showLogging = true) {
   })
 }
 
-module.exports.commandExists = async function(command) {
+export async function commandExists(command) {
   // When the command-exists module sees that a given command doesn't exist, it
   // throws an error instead of returning false, which is not what we want.
 
@@ -39,12 +37,12 @@ module.exports.commandExists = async function(command) {
   }
 }
 
-module.exports.killProcess = async function(proc) {
+export async function killProcess(proc) {
   // Windows is stupid and doesn't like it when we try to kill processes.
   // So instead we use taskkill! https://stackoverflow.com/a/28163919/4633828
 
-  if (await module.exports.commandExists('taskkill')) {
-    await module.exports.promisifyProcess(
+  if (await commandExists('taskkill')) {
+    await promisifyProcess(
       spawn('taskkill', ['/pid', proc.pid, '/f', '/t']),
       false
     )
@@ -53,18 +51,18 @@ module.exports.killProcess = async function(proc) {
   }
 }
 
-function downloadPlaylistFromURL(url) {
+export function downloadPlaylistFromURL(url) {
   return fetch(url).then(res => res.text())
 }
 
-function downloadPlaylistFromLocalPath(path) {
+export function downloadPlaylistFromLocalPath(path) {
   return readFile(path).then(buf => buf.toString())
 }
 
-module.exports.downloadPlaylistFromOptionValue = function(arg) {
+export function downloadPlaylistFromOptionValue(arg) {
   let argURL
   try {
-    argURL = new url.URL(arg)
+    argURL = new URL(arg)
   } catch (err) {
     // Definitely not a URL.
   }
@@ -73,14 +71,14 @@ module.exports.downloadPlaylistFromOptionValue = function(arg) {
     if (argURL.protocol === 'http:' || argURL.protocol === 'https:') {
       return downloadPlaylistFromURL(arg)
     } else if (argURL.protocol === 'file:') {
-      return downloadPlaylistFromLocalPath(url.fileURLToPath(argURL))
+      return downloadPlaylistFromLocalPath(fileURLToPath(argURL))
     }
   } else {
     return downloadPlaylistFromLocalPath(arg)
   }
 }
 
-module.exports.shuffleArray = function(array) {
+export function shuffleArray(array) {
   // Shuffles the items in an array. Returns a new array (does not modify the
   // passed array). Super-interesting post on how this algorithm works:
   // https://bost.ocks.org/mike/shuffle/
@@ -103,7 +101,7 @@ module.exports.shuffleArray = function(array) {
   return workingArray
 }
 
-module.exports.throttlePromise = function(maximumAtOneTime = 10) {
+export function throttlePromise(maximumAtOneTime = 10) {
   // Returns a function that takes a callback to create a promise and either
   // runs it now, if there is an available slot, or enqueues it to be run
   // later, if there is not.
@@ -139,7 +137,7 @@ module.exports.throttlePromise = function(maximumAtOneTime = 10) {
   return enqueue
 }
 
-module.exports.getSecFromTimestamp = function(timestamp) {
+export function getSecFromTimestamp(timestamp) {
   const parts = timestamp.split(':').map(n => parseInt(n))
   switch (parts.length) {
     case 3: return parts[0] * 3600 + parts[1] * 60 + parts[2]
@@ -149,7 +147,7 @@ module.exports.getSecFromTimestamp = function(timestamp) {
   }
 }
 
-module.exports.getTimeStringsFromSec = function(curSecTotal, lenSecTotal, fraction = false) {
+export function getTimeStringsFromSec(curSecTotal, lenSecTotal, fraction = false) {
   const percentVal = (100 / lenSecTotal) * curSecTotal
   const percentDone = (
     (Math.trunc(percentVal * 100) / 100).toFixed(2) + '%'
@@ -207,16 +205,16 @@ module.exports.getTimeStringsFromSec = function(curSecTotal, lenSecTotal, fracti
   return {percentDone, timeDone, timeLeft, duration, curSecTotal, lenSecTotal}
 }
 
-module.exports.getTimeStrings = function({curHour, curMin, curSec, lenHour, lenMin, lenSec}) {
+export function getTimeStrings({curHour, curMin, curSec, lenHour, lenMin, lenSec}) {
   // Multiplication casts to numbers; addition prioritizes strings.
   // Thanks, JavaScript!
   const curSecTotal = (3600 * curHour) + (60 * curMin) + (1 * curSec)
   const lenSecTotal = (3600 * lenHour) + (60 * lenMin) + (1 * lenSec)
 
-  return module.exports.getTimeStringsFromSec(curSecTotal, lenSecTotal)
+  return getTimeStringsFromSec(curSecTotal, lenSecTotal)
 }
 
-const parseOptions = async function(options, optionDescriptorMap) {
+export async function parseOptions(options, optionDescriptorMap) {
   // This function is sorely lacking in comments, but the basic usage is
   // as such:
   //
@@ -331,5 +329,3 @@ const parseOptions = async function(options, optionDescriptorMap) {
 }
 
 parseOptions.handleDashless = Symbol()
-
-module.exports.parseOptions = parseOptions
diff --git a/guess.js b/guess.js
index db9f8e8..3c72f64 100644
--- a/guess.js
+++ b/guess.js
@@ -1,21 +1,13 @@
 'use strict'
 
-const Backend = require('./backend')
-const os = require('os')
-const processSmartPlaylist = require('./smart-playlist')
-
-const {
-  flattenGrouplike,
-  parentSymbol,
-  searchForItem
-} = require('./playlist-utils')
-
-const {
-  util: {
-    ansi,
-    telchars: telc
-  }
-} = require('tui-lib')
+import os from 'node:os'
+
+import * as ansi from 'tui-lib/util/ansi'
+import telc from 'tui-lib/util/telchars'
+
+import {flattenGrouplike, parentSymbol, searchForItem} from './playlist-utils.js'
+import processSmartPlaylist from './smart-playlist.js'
+import Backend from './backend.js'
 
 function untilEvent(object, event) {
   return new Promise(resolve => {
diff --git a/index.js b/index.js
index b320812..a5930bc 100755
--- a/index.js
+++ b/index.js
@@ -2,36 +2,18 @@
 
 // omg I am tired of code
 
-const { getAllCrawlersForArg } = require('./crawlers')
-const { getPlayer } = require('./players')
-const { parseOptions } = require('./general-util')
-const AppElement = require('./ui')
-const Backend = require('./backend')
-const TelnetServer = require('./telnet')
-const processSmartPlaylist = require('./smart-playlist')
-const setupClient = require('./client')
-
-const {
-  getItemPathString,
-  updatePlaylistFormat
-} = require('./playlist-utils')
-
-const {
-  ui: {
-    Root
-  },
-  util: {
-    ansi,
-    CommandLineInterfacer,
-    Flushable
-  }
-} = require('tui-lib')
+import {getPlayer} from './players.js'
+import {parseOptions} from './general-util.js'
+import {getItemPathString} from './playlist-utils.js'
+import Backend from './backend.js'
+import setupClient from './client.js'
+import TelnetServer from './telnet.js'
+
+import {CommandLineInterface} from 'tui-lib/util/interfaces'
+import * as ansi from 'tui-lib/util/ansi'
 
-const { promisify } = require('util')
-const fs = require('fs')
-const os = require('os')
-const readFile = promisify(fs.readFile)
-const writeFile = promisify(fs.writeFile)
+import {writeFile} from 'node:fs/promises'
+import os from 'node:os'
 
 // Hack to get around errors when piping many things to stdout/err
 // (from general-util promisifyProcess)
@@ -101,7 +83,7 @@ async function main() {
 
   const { appElement, dirtyTerminal, flushable, root } = await setupClient({
     backend,
-    interfacer: new CommandLineInterfacer(),
+    screenInterface: new CommandLineInterface(),
     writable: process.stdout
   })
 
@@ -159,6 +141,7 @@ async function main() {
     root.h = h
     root.fixAllLayout()
 
+    /* eslint-disable-next-line no-unused-vars */
     const XXstress = func => '[disabled]'
 
     const stress = func => {
diff --git a/metadata-readers.js b/metadata-readers.js
index edcac72..d0f5f55 100644
--- a/metadata-readers.js
+++ b/metadata-readers.js
@@ -1,5 +1,6 @@
-const { promisifyProcess } = require('./general-util')
-const { spawn } = require('child_process')
+import {spawn} from 'node:child_process'
+
+import {promisifyProcess} from './general-util.js'
 
 // Some probers are sorta inconsistent; this function lets them try again if
 // they fail the first time.
@@ -21,8 +22,10 @@ const tryAgain = function(times, func) {
   }
 }
 
-const metadataReaders = {
-  ffprobe: tryAgain(6, async filePath => {
+const metadataReaders = {}
+
+metadataReaders.ffprobe =
+  tryAgain(6, async filePath => {
     const ffprobe = spawn('ffprobe', [
       '-print_format', 'json',
       '-show_entries', 'stream=codec_name:format',
@@ -60,11 +63,11 @@ const metadataReaders = {
       fileSize: parseInt(data.format.size),
       bitrate: parseInt(data.format.bit_rate)
     }
-  }),
+  })
 
-  getMetadataReaderFor: arg => {
-    return metadataReaders.ffprobe
-  }
-}
+export default metadataReaders
 
-module.exports = metadataReaders
+export function getMetadataReaderFor(_arg) {
+  // Only the one metadata reader implemented, so far!
+  return metadataReaders.ffprobe
+}
diff --git a/package-lock.json b/package-lock.json
index c3f88fe..7870e28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,19 +11,253 @@
       "dependencies": {
         "command-exists": "^1.2.9",
         "expand-home-dir": "0.0.3",
-        "mkdirp": "^0.5.5",
+        "mkdirp": "^3.0.1",
         "natural-orderby": "^2.0.3",
         "node-fetch": "^2.6.0",
         "open": "^7.0.3",
         "sanitize-filename": "^1.6.3",
         "shell-escape": "^0.2.0",
         "tempy": "^0.2.1",
-        "tui-lib": "^0.3.2",
-        "tui-text-editor": "^0.3.1",
-        "word-wrap": "^1.2.3"
+        "tui-lib": "^0.4.0",
+        "tui-text-editor": "^0.3.1"
       },
       "bin": {
         "mtui": "index.js"
+      },
+      "devDependencies": {
+        "eslint": "^8.40.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+      "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+      "dev": true,
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+      "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+      "dev": true,
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.5.2",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "8.40.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz",
+      "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array": {
+      "version": "0.11.8",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+      "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+      "dev": true,
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^1.2.1",
+        "debug": "^4.1.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/object-schema": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+      "dev": true
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.8.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
     "node_modules/clone": {
@@ -34,11 +268,49 @@
         "node": ">=0.8"
       }
     },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
     "node_modules/command-exists": {
       "version": "1.2.9",
       "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
       "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
     },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/crypto-random-string": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
@@ -47,6 +319,29 @@
         "node": ">=4"
       }
     },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
+    },
     "node_modules/defaults": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@@ -55,11 +350,371 @@
         "clone": "^1.0.2"
       }
     },
+    "node_modules/doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "8.40.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz",
+      "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.4.0",
+        "@eslint/eslintrc": "^2.0.3",
+        "@eslint/js": "8.40.0",
+        "@humanwhocodes/config-array": "^0.11.8",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "ajv": "^6.10.0",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.0",
+        "eslint-visitor-keys": "^3.4.1",
+        "espree": "^9.5.2",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "grapheme-splitter": "^1.0.4",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-sdsl": "^4.1.4",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.1",
+        "strip-ansi": "^6.0.1",
+        "strip-json-comments": "^3.1.0",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
+      "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+      "dev": true,
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+      "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree": {
+      "version": "9.5.2",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
+      "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.8.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/expand-home-dir": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/expand-home-dir/-/expand-home-dir-0.0.3.tgz",
       "integrity": "sha1-ct6KBIbMKKO71wRjU5iCW1tign0="
     },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
+    },
+    "node_modules/fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "dev": true,
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+      "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+      "dev": true,
+      "dependencies": {
+        "flatted": "^3.1.0",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+      "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+      "dev": true
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "13.20.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+      "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+      "dev": true,
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/grapheme-splitter": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+      "dev": true
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
     "node_modules/is-docker": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -74,6 +729,36 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-wsl": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -85,22 +770,118 @@
         "node": ">=8"
       }
     },
-    "node_modules/minimist": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true
     },
-    "node_modules/mkdirp": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
-      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+    "node_modules/js-sdsl": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
+      "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==",
+      "dev": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/js-sdsl"
+      }
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
       "dependencies": {
-        "minimist": "^1.2.6"
+        "argparse": "^2.0.1"
       },
       "bin": {
-        "mkdirp": "bin/cmd.js"
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+      "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+      "bin": {
+        "mkdirp": "dist/cjs/src/bin.js"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
     "node_modules/natural-orderby": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz",
@@ -128,6 +909,15 @@
         }
       }
     },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
     "node_modules/open": {
       "version": "7.4.2",
       "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
@@ -143,6 +933,187 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/optionator": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "dev": true,
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.3"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
     "node_modules/sanitize-filename": {
       "version": "1.6.3",
       "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
@@ -151,11 +1122,68 @@
         "truncate-utf8-bytes": "^1.0.0"
       }
     },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/shell-escape": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
       "integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM="
     },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/temp-dir": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
@@ -176,6 +1204,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true
+    },
     "node_modules/tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -190,13 +1224,22 @@
       }
     },
     "node_modules/tui-lib": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/tui-lib/-/tui-lib-0.3.3.tgz",
-      "integrity": "sha512-Cgnzpv3tl4il72spmfDQRCwEjGm2VoS8NgtOEwtFAFVj8k+gfXpIxwok3LW8Ik/vEG9qa0N1tABXf2MEzCTmhQ==",
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/tui-lib/-/tui-lib-0.4.0.tgz",
+      "integrity": "sha512-P7PgQHWNK8yVlWZbWm7XLFwirkzQzKNYkhle2YYzj1Ba7fDuh5CITDLvogKFmZSC7RiBC4Y2+2uBpNcRAf1gwQ==",
       "dependencies": {
+        "natural-orderby": "^3.0.2",
         "wcwidth": "^1.0.1"
       }
     },
+    "node_modules/tui-lib/node_modules/natural-orderby": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-3.0.2.tgz",
+      "integrity": "sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g==",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/tui-text-editor": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/tui-text-editor/-/tui-text-editor-0.3.1.tgz",
@@ -214,6 +1257,30 @@
         "word-wrap": "^1.2.3"
       }
     },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/unique-string": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
@@ -225,6 +1292,15 @@
         "node": ">=4"
       }
     },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
     "node_modules/utf8-byte-length": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
@@ -252,6 +1328,21 @@
         "webidl-conversions": "^3.0.0"
       }
     },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@@ -259,24 +1350,254 @@
       "engines": {
         "node": ">=0.10.0"
       }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
     }
   },
   "dependencies": {
+    "@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^3.3.0"
+      }
+    },
+    "@eslint-community/regexpp": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+      "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+      "dev": true
+    },
+    "@eslint/eslintrc": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+      "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.5.2",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      }
+    },
+    "@eslint/js": {
+      "version": "8.40.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz",
+      "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==",
+      "dev": true
+    },
+    "@humanwhocodes/config-array": {
+      "version": "0.11.8",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+      "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+      "dev": true,
+      "requires": {
+        "@humanwhocodes/object-schema": "^1.2.1",
+        "debug": "^4.1.1",
+        "minimatch": "^3.0.5"
+      }
+    },
+    "@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true
+    },
+    "@humanwhocodes/object-schema": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+      "dev": true
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
+    "acorn": {
+      "version": "8.8.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "requires": {}
+    },
+    "ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "requires": {
+        "color-convert": "^2.0.1"
+      }
+    },
+    "argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
+    "chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      }
+    },
     "clone": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
       "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
     },
+    "color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
     "command-exists": {
       "version": "1.2.9",
       "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
       "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
     },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
     "crypto-random-string": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
       "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4="
     },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
+    "deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
+    },
     "defaults": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@@ -285,16 +1606,307 @@
         "clone": "^1.0.2"
       }
     },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true
+    },
+    "eslint": {
+      "version": "8.40.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz",
+      "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.4.0",
+        "@eslint/eslintrc": "^2.0.3",
+        "@eslint/js": "8.40.0",
+        "@humanwhocodes/config-array": "^0.11.8",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "ajv": "^6.10.0",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.0",
+        "eslint-visitor-keys": "^3.4.1",
+        "espree": "^9.5.2",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "grapheme-splitter": "^1.0.4",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-sdsl": "^4.1.4",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.1",
+        "strip-ansi": "^6.0.1",
+        "strip-json-comments": "^3.1.0",
+        "text-table": "^0.2.0"
+      }
+    },
+    "eslint-scope": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
+      "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+      "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+      "dev": true
+    },
+    "espree": {
+      "version": "9.5.2",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
+      "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
+      "dev": true,
+      "requires": {
+        "acorn": "^8.8.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      }
+    },
+    "esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.1.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.2.0"
+      }
+    },
+    "estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
     "expand-home-dir": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/expand-home-dir/-/expand-home-dir-0.0.3.tgz",
       "integrity": "sha1-ct6KBIbMKKO71wRjU5iCW1tign0="
     },
+    "fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
+    },
+    "fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^3.0.4"
+      }
+    },
+    "find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "requires": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+      "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+      "dev": true,
+      "requires": {
+        "flatted": "^3.1.0",
+        "rimraf": "^3.0.2"
+      }
+    },
+    "flatted": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+      "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.3"
+      }
+    },
+    "globals": {
+      "version": "13.20.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+      "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.20.2"
+      }
+    },
+    "grapheme-splitter": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true
+    },
+    "ignore": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+      "dev": true
+    },
+    "import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
     "is-docker": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
       "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
     },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true
+    },
     "is-wsl": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -303,19 +1915,90 @@
         "is-docker": "^2.0.0"
       }
     },
-    "minimist": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true
     },
-    "mkdirp": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
-      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+    "js-sdsl": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
+      "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
       "requires": {
-        "minimist": "^1.2.6"
+        "argparse": "^2.0.1"
       }
     },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      }
+    },
+    "locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "requires": {
+        "p-locate": "^5.0.0"
+      }
+    },
+    "lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "mkdirp": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+      "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
     "natural-orderby": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz",
@@ -329,6 +2012,15 @@
         "whatwg-url": "^5.0.0"
       }
     },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
     "open": {
       "version": "7.4.2",
       "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
@@ -338,6 +2030,113 @@
         "is-wsl": "^2.1.1"
       }
     },
+    "optionator": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "dev": true,
+      "requires": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.3"
+      }
+    },
+    "p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "requires": {
+        "yocto-queue": "^0.1.0"
+      }
+    },
+    "p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "requires": {
+        "p-limit": "^3.0.2"
+      }
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true
+    },
+    "punycode": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+      "dev": true
+    },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
     "sanitize-filename": {
       "version": "1.6.3",
       "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
@@ -346,11 +2145,50 @@
         "truncate-utf8-bytes": "^1.0.0"
       }
     },
+    "shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true
+    },
     "shell-escape": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
       "integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM="
     },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
+    "strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true
+    },
+    "supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^4.0.0"
+      }
+    },
     "temp-dir": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
@@ -365,6 +2203,12 @@
         "unique-string": "^1.0.0"
       }
     },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true
+    },
     "tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -379,11 +2223,19 @@
       }
     },
     "tui-lib": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/tui-lib/-/tui-lib-0.3.3.tgz",
-      "integrity": "sha512-Cgnzpv3tl4il72spmfDQRCwEjGm2VoS8NgtOEwtFAFVj8k+gfXpIxwok3LW8Ik/vEG9qa0N1tABXf2MEzCTmhQ==",
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/tui-lib/-/tui-lib-0.4.0.tgz",
+      "integrity": "sha512-P7PgQHWNK8yVlWZbWm7XLFwirkzQzKNYkhle2YYzj1Ba7fDuh5CITDLvogKFmZSC7RiBC4Y2+2uBpNcRAf1gwQ==",
       "requires": {
+        "natural-orderby": "^3.0.2",
         "wcwidth": "^1.0.1"
+      },
+      "dependencies": {
+        "natural-orderby": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-3.0.2.tgz",
+          "integrity": "sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g=="
+        }
       }
     },
     "tui-text-editor": {
@@ -405,6 +2257,21 @@
         }
       }
     },
+    "type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1"
+      }
+    },
+    "type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true
+    },
     "unique-string": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
@@ -413,6 +2280,15 @@
         "crypto-random-string": "^1.0.0"
       }
     },
+    "uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
     "utf8-byte-length": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
@@ -440,10 +2316,31 @@
         "webidl-conversions": "^3.0.0"
       }
     },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
     "word-wrap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
       "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true
     }
   }
 }
diff --git a/package.json b/package.json
index a5777eb..3639c69 100644
--- a/package.json
+++ b/package.json
@@ -8,18 +8,21 @@
   },
   "author": "",
   "license": "GPL-3.0",
+  "type": "module",
   "dependencies": {
     "command-exists": "^1.2.9",
     "expand-home-dir": "0.0.3",
-    "mkdirp": "^0.5.5",
+    "mkdirp": "^3.0.1",
     "natural-orderby": "^2.0.3",
     "node-fetch": "^2.6.0",
     "open": "^7.0.3",
     "sanitize-filename": "^1.6.3",
     "shell-escape": "^0.2.0",
     "tempy": "^0.2.1",
-    "tui-lib": "^0.3.2",
-    "tui-text-editor": "^0.3.1",
-    "word-wrap": "^1.2.3"
+    "tui-lib": "^0.4.0",
+    "tui-text-editor": "^0.3.1"
+  },
+  "devDependencies": {
+    "eslint": "^8.40.0"
   }
 }
diff --git a/players.js b/players.js
index 1d64061..959bf27 100644
--- a/players.js
+++ b/players.js
@@ -1,21 +1,22 @@
 // stolen from http-music
 
-const {
+import {
   commandExists,
   killProcess,
   getTimeStrings,
-  getTimeStringsFromSec
-} = require('./general-util')
+  getTimeStringsFromSec,
+} from './general-util.js'
 
-const { spawn } = require('child_process')
-const EventEmitter = require('events')
-const Socat = require('./socat')
-const fs = require('fs')
-const util = require('util')
+import {spawn} from 'node:child_process'
+import {statSync} from 'node:fs'
+import {unlink} from 'node:fs/promises'
+import EventEmitter from 'node:events'
+import path from 'node:path'
+import url from 'node:url'
 
-const unlink = util.promisify(fs.unlink)
+import Socat from './socat.js'
 
-class Player extends EventEmitter {
+export class Player extends EventEmitter {
   constructor(processOptions = []) {
     super()
 
@@ -43,14 +44,14 @@ class Player extends EventEmitter {
     return this._process
   }
 
-  playFile(file, startTime) {}
-  seekAhead(secs) {}
-  seekBack(secs) {}
-  seekTo(timeInSecs) {}
+  playFile(_file, _startTime) {}
+  seekAhead(_secs) {}
+  seekBack(_secs) {}
+  seekTo(_timeInSecs) {}
   seekToStart() {}
-  volUp(amount) {}
-  volDown(amount) {}
-  setVolume(value) {}
+  volUp(_amount) {}
+  volDown(_amount) {}
+  setVolume(_value) {}
   updateVolume() {}
   togglePause() {}
   toggleLoop() {}
@@ -93,7 +94,7 @@ class Player extends EventEmitter {
   }
 }
 
-module.exports.MPVPlayer = class extends Player {
+export class MPVPlayer extends Player {
   // The more powerful MPV player. MPV is virtually impossible for a human
   // being to install; if you're having trouble with it, try the SoX player.
 
@@ -172,7 +173,7 @@ module.exports.MPVPlayer = class extends Player {
   }
 }
 
-module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer {
+export class ControllableMPVPlayer extends MPVPlayer {
   getMPVOptions(...args) {
     return ['--input-ipc-server=' + this.socat.path, ...super.getMPVOptions(...args)]
   }
@@ -181,8 +182,7 @@ module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer {
     this.removeSocket(this.socketPath)
 
     do {
-      // this.socketPathpath = '/tmp/mtui-socket-' + Math.floor(Math.random() * 10000)
-      this.socketPath = __dirname + '/mtui-socket-' + Math.floor(Math.random() * 10000)
+      this.socketPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), 'mtui-socket-' + Math.floor(Math.random() * 10000))
     } while (this.existsSync(this.socketPath))
 
     this.socat = new Socat(this.socketPath)
@@ -196,7 +196,7 @@ module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer {
 
   existsSync(path) {
     try {
-      fs.statSync(path)
+      statSync(path)
       return true
     } catch (error) {
       return false
@@ -289,7 +289,7 @@ module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer {
   }
 }
 
-module.exports.SoXPlayer = class extends Player {
+export class SoXPlayer extends Player {
   playFile(file, startTime) {
     // SoX's play command is useful for systems that don't have MPV. SoX is
     // much easier to install (and probably more commonly installed, as well).
@@ -315,14 +315,12 @@ module.exports.SoXPlayer = class extends Player {
           return
         }
 
-        const timeRegex = '([0-9]*):([0-9]*):([0-9]*)\.([0-9]*)'
+        const timeRegex = String.raw`([0-9]*):([0-9]*):([0-9]*)\.([0-9]*)`
         const match = data.toString().trim().match(new RegExp(
           `^In:([0-9.]+%)\\s*${timeRegex}\\s*\\[${timeRegex}\\]`
         ))
 
         if (match) {
-          const percentStr = match[1]
-
           // SoX takes a loooooot of math in order to actually figure out the
           // duration, since it outputs the current time and the remaining time
           // (but not the duration).
@@ -385,15 +383,15 @@ module.exports.SoXPlayer = class extends Player {
   }
 }
 
-module.exports.getPlayer = async function(name = null, options = []) {
+export async function getPlayer(name = null, options = []) {
   if (await commandExists('mpv') && (name === null || name === 'mpv')) {
-    return new module.exports.ControllableMPVPlayer(options)
+    return new ControllableMPVPlayer(options)
   } else if (name === 'mpv') {
     return null
   }
 
   if (await commandExists('play') && (name === null || name === 'sox')) {
-    return new module.exports.SoXPlayer(options)
+    return new SoXPlayer(options)
   } else if (name === 'sox') {
     return null
   }
diff --git a/playlist-utils.js b/playlist-utils.js
index 227c985..0fe26df 100644
--- a/playlist-utils.js
+++ b/playlist-utils.js
@@ -1,16 +1,12 @@
 'use strict'
 
-const path = require('path')
-const fs = require('fs')
+import path from 'node:path'
 
-const { promisify } = require('util')
-const unlink = promisify(fs.unlink)
+import {shuffleArray} from './general-util.js'
 
-const { shuffleArray } = require('./general-util')
+export const parentSymbol = Symbol('Parent group')
 
-const parentSymbol = Symbol('Parent group')
-
-function updatePlaylistFormat(playlist) {
+export function updatePlaylistFormat(playlist) {
   const defaultPlaylist = {
     options: [],
     items: []
@@ -43,7 +39,7 @@ function updatePlaylistFormat(playlist) {
   return updateGroupFormat(fullPlaylistObj)
 }
 
-function updateGroupFormat(group) {
+export function updateGroupFormat(group) {
   const defaultGroup = {
     name: '',
     items: []
@@ -61,7 +57,7 @@ function updateGroupFormat(group) {
 
   groupObj.items = groupObj.items.map(item => {
     // Check if it's a group; if not, it's probably a track.
-    if (typeof item[1] === 'array' || item.items) {
+    if (Array.isArray(item[1]) || item.items) {
       item = updateGroupFormat(item)
     } else {
       item = updateTrackFormat(item)
@@ -85,7 +81,7 @@ function updateGroupFormat(group) {
   return groupObj
 }
 
-function updateTrackFormat(track) {
+export function updateTrackFormat(track) {
   const defaultTrack = {
     name: '',
     downloaderArg: ''
@@ -106,7 +102,7 @@ function updateTrackFormat(track) {
   return Object.assign(defaultTrack, trackObj)
 }
 
-function cloneGrouplike(grouplike) {
+export function cloneGrouplike(grouplike) {
   const newGrouplike = {
     name: grouplike.name,
     items: grouplike.items.map(item => {
@@ -128,7 +124,7 @@ function cloneGrouplike(grouplike) {
   return newGrouplike
 }
 
-function filterTracks(grouplike, handleTrack) {
+export function filterTracks(grouplike, handleTrack) {
   // Recursively filters every track in the passed grouplike. The track-handler
   // function passed should either return true (to keep a track) or false (to
   // remove the track). After tracks are filtered, groups which contain no
@@ -161,7 +157,7 @@ function filterTracks(grouplike, handleTrack) {
   })
 }
 
-function flattenGrouplike(grouplike) {
+export function flattenGrouplike(grouplike) {
   // Flattens a group-like, taking all of the non-group items (tracks) at all
   // levels in the group tree and returns them as a new group containing those
   // tracks.
@@ -177,7 +173,7 @@ function flattenGrouplike(grouplike) {
   }
 }
 
-function countTotalTracks(item) {
+export function countTotalTracks(item) {
   // Returns the total number of tracks in a grouplike, including tracks in any
   // descendant groups. Basically the same as flattenGrouplike().items.length.
 
@@ -191,7 +187,7 @@ function countTotalTracks(item) {
   }
 }
 
-function shuffleOrderOfGroups(grouplike) {
+export function shuffleOrderOfGroups(grouplike) {
   // OK, this is opinionated on how it should work, but I think it Makes Sense.
   // Also sorry functional-programming friends, I'm sure this is a horror.
   // (FYI, this is the same as how http-music used to work with shuffle-groups,
@@ -209,12 +205,12 @@ function shuffleOrderOfGroups(grouplike) {
   return {items: shuffleArray(items)}
 }
 
-function reverseOrderOfGroups(grouplike) {
+export function reverseOrderOfGroups(grouplike) {
   const { items } = collapseGrouplike(grouplike)
   return {items: items.reverse()}
 }
 
-function collectGrouplikeChildren(grouplike, filter = null) {
+export function collectGrouplikeChildren(grouplike, filter = null) {
   // Collects all descendants of a grouplike into a single flat array.
   // Can be passed a filter function, which will decide whether or not to add
   // an item to the return array. However, note that all descendants will be
@@ -237,7 +233,7 @@ function collectGrouplikeChildren(grouplike, filter = null) {
   return items
 }
 
-function partiallyFlattenGrouplike(grouplike, resultDepth) {
+export function partiallyFlattenGrouplike(grouplike, resultDepth) {
   // Flattens a grouplike so that it is never more than a given number of
   // groups deep, INCLUDING the "top" group -- e.g. a resultDepth of 2
   // means that there can be one level of groups remaining in the resulting
@@ -258,7 +254,7 @@ function partiallyFlattenGrouplike(grouplike, resultDepth) {
   return {items}
 }
 
-function collapseGrouplike(grouplike) {
+export function collapseGrouplike(grouplike) {
   // Similar to partiallyFlattenGrouplike, but doesn't discard the individual
   // ordering of tracks; rather, it just collapses them all to one level.
 
@@ -284,7 +280,7 @@ function collapseGrouplike(grouplike) {
   return {items: ret}
 }
 
-function filterGrouplikeByProperty(grouplike, property, value) {
+export function filterGrouplikeByProperty(grouplike, property, value) {
   // Returns a copy of the original grouplike, only keeping tracks with the
   // given property-value pair. (If the track's value for the given property
   // is an array, this will check if that array includes the given value.)
@@ -314,13 +310,13 @@ function filterGrouplikeByProperty(grouplike, property, value) {
   })
 }
 
-function filterPlaylistByPathString(playlist, pathString) {
+export function filterPlaylistByPathString(playlist, pathString) {
   // Calls filterGroupContentsByPath, taking an unparsed path string.
 
   return filterGrouplikeByPath(playlist, parsePathString(pathString))
 }
 
-function filterGrouplikeByPath(grouplike, pathParts) {
+export function filterGrouplikeByPath(grouplike, pathParts) {
   // Finds a group by following the given group path and returns it. If the
   // function encounters an item in the group path that is not found, it logs
   // a warning message and returns the group found up to that point. If the
@@ -371,13 +367,13 @@ function filterGrouplikeByPath(grouplike, pathParts) {
   }
 }
 
-function removeGroupByPathString(playlist, pathString) {
+export function removeGroupByPathString(playlist, pathString) {
   // Calls removeGroupByPath, taking a path string, rather than a parsed path.
 
   return removeGroupByPath(playlist, parsePathString(pathString))
 }
 
-function removeGroupByPath(playlist, pathParts) {
+export function removeGroupByPath(playlist, pathParts) {
   // Removes the group at the given path from the given playlist.
 
   const groupToRemove = filterGrouplikeByPath(playlist, pathParts)
@@ -418,7 +414,7 @@ function removeGroupByPath(playlist, pathParts) {
   }
 }
 
-function getPlaylistTreeString(playlist, showTracks = false) {
+export function getPlaylistTreeString(playlist, showTracks = false) {
   function recursive(group) {
     const groups = group.items.filter(x => isGroup(x))
     const nonGroups = group.items.filter(x => !isGroup(x))
@@ -454,7 +450,7 @@ function getPlaylistTreeString(playlist, showTracks = false) {
   return recursive(playlist)
 }
 
-function getItemPath(item) {
+export function getItemPath(item) {
   if (item[parentSymbol]) {
     return [...getItemPath(item[parentSymbol]), item]
   } else {
@@ -462,7 +458,7 @@ function getItemPath(item) {
   }
 }
 
-function getItemPathString(item) {
+export function getItemPathString(item) {
   // Gets the playlist path of an item by following its parent chain.
   //
   // Returns a string in format Foo/Bar/Baz, where Foo and Bar are group
@@ -489,12 +485,12 @@ function getItemPathString(item) {
   }
 }
 
-function parsePathString(pathString) {
+export function parsePathString(pathString) {
   const pathParts = pathString.split('/').filter(item => item.length)
   return pathParts
 }
 
-function getTrackIndexInParent(track) {
+export function getTrackIndexInParent(track) {
   if (parentSymbol in track === false) {
     throw new Error(
       'getTrackIndexInParent called with a track that has no parent!'
@@ -505,6 +501,11 @@ function getTrackIndexInParent(track) {
 
   let i = 0, foundTrack = false;
   for (; i < parent.items.length; i++) {
+    // TODO: Port isSameTrack from http-music, if it makes sense - doing
+    // so involves porting the [oldSymbol] property on all tracks and groups,
+    // so may or may not be the right call. This function isn't used anywhere
+    // in mtui so it'll take a little extra investigation.
+    /* eslint-disable-next-line no-undef */
     if (isSameTrack(track, parent.items[i])) {
       foundTrack = true
       break
@@ -519,14 +520,14 @@ function getTrackIndexInParent(track) {
 }
 
 const nameWithoutTrackNumberSymbol = Symbol('Cached name without track number')
-function getNameWithoutTrackNumber(track) {
+export function getNameWithoutTrackNumber(track) {
   // A "part" is a series of numeric digits, separated from other parts by
   // whitespace, dashes, and dots, always preceding either the first non-
   // numeric/separator character or (if there are no such characters) the
   // first word (i.e. last whitespace).
   const getNumberOfParts = ({ name }) => {
-    name = name.replace(/^[\-\s.]+$/, '')
-    const match = name.match(/[^0-9\-\s.]/)
+    name = name.replace(/^[-\s.]+$/, '')
+    const match = name.match(/[^0-9-\s.]/)
     if (match) {
       if (match.index === 0) {
         return 0
@@ -538,12 +539,12 @@ function getNameWithoutTrackNumber(track) {
     } else {
       return 0
     }
-    name = name.replace(/[\-\s.]+$/, '')
-    return name.split(/[\-\s.]+/g).length
+    name = name.replace(/[-\s.]+$/, '')
+    return name.split(/[-\s.]+/g).length
   }
 
   const removeParts = (name, numParts) => {
-    const regex = new RegExp(String.raw`[\-\s.]{0,}([0-9]+[\-\s.]+){${numParts},${numParts}}`)
+    const regex = new RegExp(String.raw`[-\s.]{0,}([0-9]+[-\s.]+){${numParts},${numParts}}`)
     return track.name.replace(regex, '')
   }
 
@@ -591,24 +592,24 @@ function getNameWithoutTrackNumber(track) {
   }
 }
 
-function isGroup(obj) {
+export function isGroup(obj) {
   return !!(obj && obj.items)
 }
 
-function isTrack(obj) {
+export function isTrack(obj) {
   return !!(obj && obj.downloaderArg)
 }
 
-function isPlayable(obj) {
+export function isPlayable(obj) {
   return isGroup(obj) || isTrack(obj)
 }
 
-function isOpenable(obj) {
+export function isOpenable(obj) {
   return !!(obj && obj.url)
 }
 
 
-function searchForItem(grouplike, value, preferredStartIndex = -1) {
+export function searchForItem(grouplike, value, preferredStartIndex = -1) {
   if (value.length) {
     // We prioritize searching past the index that the user opened the jump
     // element from (oldFocusedIndex). This is so that it's more practical
@@ -648,7 +649,7 @@ function searchForItem(grouplike, value, preferredStartIndex = -1) {
   return null
 }
 
-function getCorrespondingFileForItem(item, extension) {
+export function getCorrespondingFileForItem(item, extension) {
   if (!(item && item.url)) {
     return null
   }
@@ -673,7 +674,7 @@ function getCorrespondingFileForItem(item, extension) {
   return null
 }
 
-function getCorrespondingPlayableForFile(item) {
+export function getCorrespondingPlayableForFile(item) {
   if (!(item && item.url)) {
     return null
   }
@@ -691,53 +692,3 @@ function getCorrespondingPlayableForFile(item) {
   const basename = path.basename(item.url, path.extname(item.url))
   return parent.items.find(item => isPlayable(item) && path.basename(item.url, path.extname(item.url)) === basename)
 }
-
-module.exports = {
-  parentSymbol,
-  updatePlaylistFormat, updateGroupFormat, updateTrackFormat,
-  cloneGrouplike,
-  filterTracks,
-  flattenGrouplike, countTotalTracks,
-  shuffleOrderOfGroups,
-  reverseOrderOfGroups,
-  partiallyFlattenGrouplike, collapseGrouplike,
-  filterGrouplikeByProperty,
-  filterPlaylistByPathString, filterGrouplikeByPath,
-  removeGroupByPathString, removeGroupByPath,
-  getPlaylistTreeString,
-  getItemPath, getItemPathString,
-  parsePathString,
-  getTrackIndexInParent,
-  getNameWithoutTrackNumber,
-  searchForItem,
-  getCorrespondingFileForItem,
-  getCorrespondingPlayableForFile,
-  isGroup, isTrack,
-  isOpenable, isPlayable
-}
-
-if (require.main === module) {
-  {
-    const group = updateGroupFormat({items: [
-      {name: '- 1.01 Hello World 425', downloaderArg: 'x'},
-      {name: '1.02 Aww Yeah 371', downloaderArg: 'x'},
-      {name: ' 1.03 Here Goes 472', downloaderArg: 'x'}
-    ]})
-
-    for (let i = 0; i < group.items.length; i++) {
-      console.log(group.items[i].name, '->', getNameWithoutTrackNumber(group.items[i]))
-    }
-  }
-
-  {
-    const group = updateGroupFormat({items: [
-      {name: 'BAM #1', downloaderArg: 'x'},
-      {name: 'BAM #2', downloaderArg: 'x'},
-      {name: 'BAM #3.1 - no', downloaderArg: 'x'}
-    ]})
-
-    for (let i = 0; i < group.items.length; i++) {
-      console.log(group.items[i].name, '->', getNameWithoutTrackNumber(group.items[i]))
-    }
-  }
-}
diff --git a/record-store.js b/record-store.js
index 80c8d3a..2686457 100644
--- a/record-store.js
+++ b/record-store.js
@@ -1,6 +1,6 @@
 const recordSymbolKey = Symbol('Record symbol')
 
-module.exports = class RecordStore {
+export default class RecordStore {
   constructor() {
     // Each track (or whatever) gets a symbol which is used as a key here to
     // store more information.
diff --git a/smart-playlist.js b/smart-playlist.js
index 19294db..c8abf62 100644
--- a/smart-playlist.js
+++ b/smart-playlist.js
@@ -1,7 +1,7 @@
-const { getCrawlerByName } = require('./crawlers')
-const { isGroup, filterTracks, sourceSymbol, updatePlaylistFormat } = require('./playlist-utils')
+import {getCrawlerByName} from './crawlers.js'
+import {filterTracks, isGroup, updatePlaylistFormat} from './playlist-utils.js'
 
-async function processSmartPlaylist(item, topItem = true) {
+export default async function processSmartPlaylist(item, topItem = true) {
   // Object.assign is used so that we keep original properties, e.g. "name"
   // or "apply". (It's also used so we return copies of original objects.)
 
@@ -133,5 +133,3 @@ async function processSmartPlaylist(item, topItem = true) {
     return newItem
   }
 }
-
-module.exports = processSmartPlaylist
diff --git a/socat.js b/socat.js
index 8871c7e..a465a73 100644
--- a/socat.js
+++ b/socat.js
@@ -2,16 +2,14 @@
 // Assumes access to the `socat` command as a child process; if it's not
 // present, it will fall back to just writing to the specified file.
 
-const EventEmitter = require('events')
-const { spawn } = require('child_process')
-const { killProcess, commandExists } = require('./general-util')
-const { promisify } = require('util')
-const fs = require('fs')
-const path = require('path')
+import {spawn} from 'node:child_process'
+import {writeFile} from 'node:fs/promises'
+import EventEmitter from 'node:events'
+import path from 'node:path'
 
-const writeFile = promisify(fs.writeFile)
+import {killProcess, commandExists} from './general-util.js'
 
-module.exports = class Socat extends EventEmitter {
+export default class Socat extends EventEmitter {
   constructor(path) {
     super()
     this.setPath(path)
@@ -30,7 +28,7 @@ module.exports = class Socat extends EventEmitter {
       this.subprocess.on('close', () => {
         this.subprocess = null
       })
-      this.subprocess.stdin.on('error', err => {
+      this.subprocess.stdin.on('error', () => {
         this.stop()
       })
     }
@@ -69,7 +67,7 @@ module.exports = class Socat extends EventEmitter {
       }
     } else {
       try {
-        await writeFile(path.resolve(__dirname, this.path), message + '\r\n')
+        await writeFile(path.resolve(process.cwd(), this.path), message + '\r\n')
       } catch (error) {
         // :shrug: We tried!
         // -- It's possible to get here if the specified path isn't an actual
diff --git a/telnet.js b/telnet.js
index 33e3dcc..42e664d 100644
--- a/telnet.js
+++ b/telnet.js
@@ -1,16 +1,11 @@
-'use strict'
+import EventEmitter from 'node:events'
+import net from 'node:net'
 
-const EventEmitter = require('events')
-const net = require('net')
-const setupClient = require('./client')
+import {TelnetInterface} from 'tui-lib/util/interfaces'
 
-const {
-  util: {
-    TelnetInterfacer
-  }
-} = require('tui-lib')
+import setupClient from './client.js'
 
-class TelnetServer extends EventEmitter {
+export default class TelnetServer extends EventEmitter {
   constructor(backend) {
     super()
 
@@ -24,11 +19,11 @@ class TelnetServer extends EventEmitter {
   }
 
   async handleConnection(socket) {
-    const interfacer = new TelnetInterfacer(socket)
+    const telnetInterface = new TelnetInterface(socket)
     const { appElement, cleanTerminal, flushable } = await setupClient({
       backend: this.backend,
       writable: socket,
-      interfacer,
+      screenInterface: telnetInterface,
       appConfig: {
         canControlPlayback: false,
         canControlQueue: true,
@@ -47,7 +42,7 @@ class TelnetServer extends EventEmitter {
 
     const quit = (msg = 'See you!') => {
       cleanTerminal()
-      interfacer.cleanTelnetOptions()
+      telnetInterface.cleanTelnetOptions()
       socket.write('\r' + msg + '\r\n')
       socket.end()
       flushable.end()
@@ -77,5 +72,3 @@ class TelnetServer extends EventEmitter {
     }
   }
 }
-
-module.exports = TelnetServer
diff --git a/todo.txt b/todo.txt
index b10c614..4c93789 100644
--- a/todo.txt
+++ b/todo.txt
@@ -690,3 +690,9 @@ TODO: Apparently pressing any key while the UI is booting up will make the
       screen totally black and unresponsive (and apparently inactive) until the
       screen is resized. I think we're interrupting a control sequence somehow,
       and that isn't being handled very well?
+
+TODO: Pressing escape while you've got items selected should deselect those
+      items, rather than stop playback! ...Or SHOULD IT??? Well, yes. But it's
+      still handy to not be locked out of stopping playback altogether.
+      Alternative: clear the selection (without stopping playback) only if the
+      cursor is currently on a selected item.
diff --git a/ui.js b/ui.js
index dc9dab9..f006a70 100644
--- a/ui.js
+++ b/ui.js
@@ -1,20 +1,35 @@
 // The UI in MTUI! Interfaces with the backend to form the complete mtui app.
 
-'use strict'
+import {spawn} from 'node:child_process'
+import {readFile, writeFile} from 'node:fs/promises'
+import path from 'node:path'
+import url from 'node:url'
 
-const { getAllCrawlersForArg } = require('./crawlers')
-const processSmartPlaylist = require('./smart-playlist')
-const UndoManager = require('./undo-manager')
+import {orderBy} from 'natural-orderby'
+import open from 'open'
 
-const {
+import {Button, Form, ListScrollForm, TextInput} from 'tui-lib/ui/controls'
+import {Dialog} from 'tui-lib/ui/dialogs'
+import {Label, Pane, WrapLabel} from 'tui-lib/ui/presentation'
+import {DisplayElement, FocusElement} from 'tui-lib/ui/primitives'
+
+import * as ansi from 'tui-lib/util/ansi'
+import telc from 'tui-lib/util/telchars'
+import unic from 'tui-lib/util/unichars'
+
+import {getAllCrawlersForArg} from './crawlers.js'
+import processSmartPlaylist from './smart-playlist.js'
+import UndoManager from './undo-manager.js'
+
+import {
   commandExists,
   getSecFromTimestamp,
   getTimeStringsFromSec,
   promisifyProcess,
-  shuffleArray
-} = require('./general-util')
+  shuffleArray,
+} from './general-util.js'
 
-const {
+import {
   cloneGrouplike,
   collapseGrouplike,
   countTotalTracks,
@@ -30,46 +45,14 @@ const {
   parentSymbol,
   reverseOrderOfGroups,
   searchForItem,
-  shuffleOrderOfGroups
-} = require('./playlist-utils')
-
-const {
-  ui: {
-    Dialog,
-    DisplayElement,
-    Label,
-    Pane,
-    WrapLabel,
-    form: {
-      Button,
-      FocusElement,
-      Form,
-      ListScrollForm,
-      TextInput,
-    }
-  },
-  util: {
-    ansi,
-    telchars: telc,
-    unichars: unic,
-  }
-} = require('tui-lib')
+  shuffleOrderOfGroups,
+} from './playlist-utils.js'
 
 /* text editor features disabled because theyre very much incomplete and havent
  * gotten much use from me or anyonea afaik!
 const TuiTextEditor = require('tui-text-editor')
 */
 
-const { promisify } = require('util')
-const { spawn } = require('child_process')
-const { orderBy } = require('natural-orderby')
-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 = {}
 
 const keyBindings = [
@@ -186,7 +169,7 @@ telc.isRight = input.isRight
 telc.isSelect = input.isSelect
 telc.isBackspace = input.isBackspace
 
-class AppElement extends FocusElement {
+export default class AppElement extends FocusElement {
   constructor(backend, config = {}) {
     super()
 
@@ -269,7 +252,7 @@ class AppElement extends FocusElement {
     this.queueTimeLabel = new Label('')
     this.queuePane.addChild(this.queueTimeLabel)
 
-    this.queueListingElement.on('select', item => this.updateQueueLengthLabel())
+    this.queueListingElement.on('select', _item => this.updateQueueLengthLabel())
     this.queueListingElement.on('open', item => this.openSpecialOrThroughSystem(item))
     this.queueListingElement.on('queue', item => this.play(item))
     this.queueListingElement.on('remove', item => this.unqueue(item))
@@ -441,7 +424,7 @@ class AppElement extends FocusElement {
 
     this.autoDJControl = new ToggleControl('Enable Auto-DJ?', {
       setValue: val => (this.enableAutoDJ = val),
-      getValue: val => this.enableAutoDJ,
+      getValue: () => this.enableAutoDJ,
       getEnabled: () => this.config.canControlPlayback
     })
 
@@ -1374,9 +1357,9 @@ class AppElement extends FocusElement {
   }
 
   showMenuForItemElement(el, listing) {
-    const { editMode } = this
+    // const { editMode } = this
     const { canControlQueue, canProcessMetadata } = this.config
-    const anyMarked = editMode && this.markGrouplike.items.length > 0
+    // const anyMarked = editMode && this.markGrouplike.items.length > 0
 
     const generatePageForItem = item => {
       const emitControls = play => () => {
@@ -1387,7 +1370,7 @@ class AppElement extends FocusElement {
         })
       }
 
-      const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt')
+      // const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt')
       const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing)
         ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)}
         : {label: 'Expand saved timestamps', action: () => this.expandTimestamps(item, listing)}
@@ -1431,13 +1414,12 @@ class AppElement extends FocusElement {
           // to move the "mark"/"paste" (etc) code into separate functions,
           // instead of just defining their behavior inside the listing event
           // handlers.
-          /*
-          editMode && {label: isMarked ? 'Unmark' : 'Mark', action: () => el.emit('mark')},
-          anyMarked && {label: 'Paste (above)', action: () => el.emit('paste', {where: 'above'})},
-          anyMarked && {label: 'Paste (below)', action: () => el.emit('paste', {where: 'below'})},
+
+          // editMode && {label: isMarked ? 'Unmark' : 'Mark', action: () => el.emit('mark')},
+          // anyMarked && {label: 'Paste (above)', action: () => el.emit('paste', {where: 'above'})},
+          // anyMarked && {label: 'Paste (below)', action: () => el.emit('paste', {where: 'below'})},
           // 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)
-          {divider: true},
-          */
+          // {divider: true},
 
           canControlQueue && isPlayable(item) && {element: this.whereControl},
           canControlQueue && isGroup(item) && {element: this.orderControl},
@@ -1450,10 +1432,8 @@ class AppElement extends FocusElement {
           canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => setTimeout(() => this.processMetadata(item, true))},
           isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)},
           isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)},
-          /*
-          !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)},
-          hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)},
-          */
+          // !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)},
+          // hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)},
           canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
           isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)},
           {divider: true},
@@ -1475,7 +1455,7 @@ class AppElement extends FocusElement {
     ].filter(Boolean)
 
     // TODO: Implement this! :P
-    const isMarked = false
+    // const isMarked = false
 
     this.showContextMenu({
       x: el.absLeft,
@@ -2250,7 +2230,7 @@ class AppElement extends FocusElement {
   }
 
   get selectedQueuePlayer() { return this.getDep('selectedQueuePlayer') }
-  set selectedQueuePlayer(v) { return this.setDep('selectedQueuePlayer', v) }
+  set selectedQueuePlayer(v) { this.setDep('selectedQueuePlayer', v) }
 }
 
 class GrouplikeListingElement extends Form {
@@ -2745,6 +2725,7 @@ class GrouplikeListingElement extends Form {
 
     // Just to make the selected-track-info bar fill right away (if it wasn't
     // already filled by a previous this.curIndex set).
+    /* eslint-disable-next-line no-self-assign */
     form.curIndex = form.curIndex
 
     this.fixAllLayout()
@@ -2916,7 +2897,6 @@ class GrouplikeListingForm extends ListScrollForm {
   set curIndex(newIndex) {
     this.setDep('curIndex', newIndex)
     this.emit('select', this.inputs[this.curIndex])
-    return newIndex
   }
 
   get curIndex() {
@@ -3007,7 +2987,6 @@ class GrouplikeListingForm extends ListScrollForm {
   }
 
   dragLeftRange(item) {
-    const { items } = this.app.markGrouplike
     if (this.selectMode === 'select') {
       if (!this.oldMarkedItems.includes(item)) {
         this.app.unmarkItem(item)
@@ -3331,8 +3310,8 @@ class InlineListPickerElement extends FocusElement {
     this.showContextMenu({
       x: this.absLeft + ansi.measureColumns(this.labelText) + 1,
       y: this.absTop + 1,
-      items: this.options.map(({ value, label }, index) => ({
-        label: label,
+      items: this.options.map(({ label }, index) => ({
+        label,
         action: () => {
           this.curIndex = index
         },
@@ -3378,7 +3357,7 @@ class InlineListPickerElement extends FocusElement {
   }
 
   get curIndex() { return this.getDep('curIndex') }
-  set curIndex(v) { return this.setDep('curIndex', v) }
+  set curIndex(v) { this.setDep('curIndex', v) }
 }
 
 // Quite hacky, but ATM I can't think of any way to neatly tie getDep/setDep
@@ -3860,7 +3839,7 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement {
     } else if (!this.isPlayable) {
       writable.write('F')
     } else if (record.downloading) {
-      writable.write(braille[Math.floor(Date.now() / 250) % 6])
+      writable.write(brailleChar)
     } else if (this.app.SQP.playingTrack === this.item) {
       writable.write('\u25B6')
     } else if (this.app.hasTimestampsFile(this.item)) {
@@ -3905,7 +3884,6 @@ class TimestampGrouplikeItemElement extends BasicGrouplikeItemElement {
       || last.timestampEnd !== Infinity && last.timestampEnd
       || last.timestamp)
     const strings = getTimeStringsFromSec(data.timestamp, duration)
-    const stringsEnd = getTimeStringsFromSec(data.timestampEnd, duration)
 
     this.text = (
       /*
@@ -4319,7 +4297,6 @@ class PlaybackInfoElement extends FocusElement {
         this.app.backend.queuePlayers.length > 1 && {
           label: 'Delete',
           action: () => {
-            const { parent } = this
             this.app.removeQueuePlayer(this.queuePlayer)
           }
         }
@@ -4414,17 +4391,17 @@ class PlaybackInfoElement extends FocusElement {
   }
 
   get curSecTotal() { return this.getDep('curSecTotal') }
-  set curSecTotal(v) { return this.setDep('curSecTotal', v) }
+  set curSecTotal(v) { this.setDep('curSecTotal', v) }
   get lenSecTotal() { return this.getDep('lenSecTotal') }
-  set lenSecTotal(v) { return this.setDep('lenSecTotal', v) }
+  set lenSecTotal(v) { this.setDep('lenSecTotal', v) }
   get volume() { return this.getDep('volume') }
-  set volume(v) { return this.setDep('volume', v) }
+  set volume(v) { this.setDep('volume', v) }
   get isLooping() { return this.getDep('isLooping') }
-  set isLooping(v) { return this.setDep('isLooping', v) }
+  set isLooping(v) { this.setDep('isLooping', v) }
   get isPaused() { return this.getDep('isPaused') }
-  set isPaused(v) { return this.setDep('isPaused', v) }
+  set isPaused(v) { this.setDep('isPaused', v) }
   get currentTrack() { return this.getDep('currentTrack') }
-  set currentTrack(v) { return this.setDep('currentTrack', v) }
+  set currentTrack(v) { this.setDep('currentTrack', v) }
 }
 
 class OpenPlaylistDialog extends Dialog {
@@ -5259,9 +5236,9 @@ class Menubar extends ListScrollForm {
   }
 
   get color() { return this.getDep('color') }
-  set color(v) { return this.setDep('color', v) }
+  set color(v) { this.setDep('color', v) }
   get attribute() { return this.getDep('attribute') }
-  set attribute(v) { return this.setDep('attribute', v) }
+  set attribute(v) { this.setDep('attribute', v) }
 }
 
 class PartyBanner extends DisplayElement {
@@ -5396,5 +5373,3 @@ class NotesTextEditor extends TuiTextEditor {
   }
 }
 */
-
-module.exports = AppElement
diff --git a/undo-manager.js b/undo-manager.js
index 4a042ad..9b53c2d 100644
--- a/undo-manager.js
+++ b/undo-manager.js
@@ -1,4 +1,4 @@
-class UndoManager {
+export default class UndoManager {
   constructor() {
     this.actionStack = []
     this.undoneStack = []
@@ -38,5 +38,3 @@ class UndoManager {
     return this.undoStack.length === 0
   }
 }
-
-module.exports = UndoManager