« get me outta code hell

Merge branch 'main' into merge-socket-mtui - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-05-16 22:41:33 -0300
committer(quasar) nebula <qznebula@protonmail.com>2024-05-16 22:41:33 -0300
commitcd91ccbea9e76ee1de1e3cbcfe17d8a2cb6ef9b4 (patch)
treed98e0b2a100170c7f913da57fb8e010c4b581603
parent2a270f20cc8e2f2a0754d28d6892bb1bd27e45ce (diff)
parenteb6a997df675fc8176fb15f34450a0c1b8416edc (diff)
Merge branch 'main' into merge-socket-mtui merge-socket-mtui
So we made this merge commit *after* going through a whole
fidanglin' bunch of steps to flat-out rebase socket-mtui...
but of course, that was pretty hopeless from the start to
get just quite right. After all, we didn't know the exact
point of each commit and how to test out the changes, so
we couldn't make sure all our rebased commits were working
the same way. (This is maybe the single coolest reason to
make sure that automated tests *pass with 100% coverage
at every commit*, but obviously this project doesn't have
that. Alas!)

This merge commit follows up a different merge commit we
actually made almost exactly one year ago to this day
(also: our birthday LOL). We figure the testing done at
that point was quite a bit more thorough than we'd do
today, and anyway there's little reason to repeat the
work we did in that commit. Comparatively, this merge
commit is way smaller!

It was still fun to go through the whole rebasing
process, even if it didn't practically bring us
anywhere. You know, assuming the merge commit from
last year didn't accidentally destroy any code or
todos lol........ *prays* ^____^
-rw-r--r--downloaders.js19
-rw-r--r--general-util.js12
-rw-r--r--package-lock.json890
-rw-r--r--package.json5
-rw-r--r--playlist-utils.js25
-rw-r--r--tempdir.js86
-rw-r--r--todo.txt6
-rw-r--r--ui.js220
8 files changed, 925 insertions, 338 deletions
diff --git a/downloaders.js b/downloaders.js
index 9e7c786..8dbdea8 100644
--- a/downloaders.js
+++ b/downloaders.js
@@ -8,9 +8,9 @@ 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'
+import temporaryDirectory from './tempdir.js'
 
 const copyFile = (source, target) => {
   // Stolen from https://stackoverflow.com/a/30405105/4633828
@@ -110,9 +110,10 @@ downloaders.http =
       return hostname + pathname
     },
     arg => {
-      const out = (
-        tempy.directory() + '/' +
-        sanitize(decodeURIComponent(path.basename(arg))))
+      const out =
+        path.join(
+          temporaryDirectory(),
+          sanitize(decodeURIComponent(path.basename(arg))))
 
       return fetch(arg)
         .then(response => response.buffer())
@@ -124,8 +125,8 @@ downloaders.youtubedl =
   cachify('youtubedl',
     arg => (arg.match(/watch\?v=(.*)/) || ['', arg])[1],
     arg => {
-      const outDir = tempy.directory()
-      const outFile = outDir + '/%(id)s-%(uploader)s-%(title)s.%(ext)s'
+      const outDir = temporaryDirectory()
+      const outFile = path.join(outDir, '%(id)s-%(uploader)s-%(title)s.%(ext)s')
 
       const opts = [
         '--quiet',
@@ -138,7 +139,7 @@ downloaders.youtubedl =
 
       return promisifyProcess(spawn('youtube-dl', opts))
         .then(() => readdir(outDir))
-        .then(files => outDir + '/' + files[0])
+        .then(files => path.join(outDir, files[0]))
     })
 
 downloaders.local =
@@ -166,7 +167,7 @@ downloaders.local =
       // TODO: Is it necessary to sanitize here?
       // Haha, the answer to "should I sanitize" is probably always YES..
       const base = path.basename(arg, path.extname(arg))
-      const out = tempy.directory() + '/' + sanitize(base) + path.extname(arg)
+      const out = path.join(temporaryDirectory(), sanitize(base) + path.extname(arg))
 
       return copyFile(arg, out)
         .then(() => out)
@@ -180,7 +181,7 @@ downloaders.locallink =
 
       arg = removeFileProtocol(arg)
       const base = path.basename(arg, path.extname(arg))
-      const out = tempy.directory() + '/' + sanitize(base) + path.extname(arg)
+      const out = path.join(temporaryDirectory(), sanitize(base) + path.extname(arg))
 
       return symlink(path.resolve(arg), out)
         .then(() => out)
diff --git a/general-util.js b/general-util.js
index d369848..78843f9 100644
--- a/general-util.js
+++ b/general-util.js
@@ -78,7 +78,7 @@ export function downloadPlaylistFromOptionValue(arg) {
   }
 }
 
-export function shuffleArray(array) {
+export function shuffleArray(array, limit = array.length) {
   // 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/
@@ -86,10 +86,12 @@ export function shuffleArray(array) {
   const workingArray = array.slice(0)
 
   let m = array.length
+  let n = limit
 
-  while (m) {
+  while (n) {
     let i = Math.floor(Math.random() * m)
     m--
+    n--
 
     // Stupid lol; avoids the need of a temporary variable!
     Object.assign(workingArray, {
@@ -98,7 +100,11 @@ export function shuffleArray(array) {
     })
   }
 
-  return workingArray
+  if (limit === array.length) {
+    return workingArray
+  } else {
+    return workingArray.slice(0, limit)
+  }
 }
 
 export function throttlePromise(maximumAtOneTime = 10) {
diff --git a/package-lock.json b/package-lock.json
index 25df086..d5a2ec0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,9 +12,11 @@
         "command-exists": "^1.2.9",
         "expand-home-dir": "0.0.3",
         "mkdirp": "^3.0.1",
+        "nanoid": "^5.0.7",
         "natural-orderby": "^2.0.3",
         "node-fetch": "^2.6.0",
-        "open": "^7.0.4",
+        "open": "^10.1.0",
+        "rimraf": "^5.0.6",
         "sanitize-filename": "^1.6.3",
         "shell-escape": "^0.2.0",
         "shortid": "^2.2.15",
@@ -31,9 +33,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "eslint-visitor-keys": "^3.3.0"
       },
@@ -46,18 +47,16 @@
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "dependencies": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
@@ -78,18 +77,16 @@
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "Apache-2.0",
       "dependencies": {
         "@humanwhocodes/object-schema": "^1.2.1",
         "debug": "^4.1.1",
@@ -101,9 +98,8 @@
     },
     "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,
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=12.22"
       },
@@ -114,15 +110,51 @@
     },
     "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
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
         "run-parallel": "^1.1.9"
@@ -133,18 +165,16 @@
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
         "fastq": "^1.6.0"
@@ -153,11 +183,18 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "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,
+      "license": "MIT",
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -167,18 +204,16 @@
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -192,18 +227,14 @@
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "dependencies": {
         "color-convert": "^2.0.1"
       },
@@ -216,40 +247,47 @@
     },
     "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
+      "dev": true,
+      "license": "Python-2.0"
     },
     "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
+      "license": "MIT"
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
       }
     },
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "run-applescript": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
@@ -270,9 +308,7 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "color-name": "~1.1.4"
       },
@@ -282,9 +318,7 @@
     },
     "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
+      "license": "MIT"
     },
     "node_modules/command-exists": {
       "version": "1.2.9",
@@ -292,15 +326,12 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -312,16 +343,16 @@
     },
     "node_modules/crypto-random-string": {
       "version": "1.0.0",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
+      "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==",
       "engines": {
         "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,
+      "license": "MIT",
       "dependencies": {
         "ms": "2.1.2"
       },
@@ -336,9 +367,32 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/default-browser": {
+      "version": "5.2.1",
+      "license": "MIT",
+      "dependencies": {
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/default-browser-id": {
+      "version": "5.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
     },
     "node_modules/defaults": {
       "version": "1.0.3",
@@ -347,11 +401,20 @@
         "clone": "^1.0.2"
       }
     },
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/doctrine": {
       "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
-      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "esutils": "^2.0.2"
       },
@@ -359,11 +422,18 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "license": "MIT"
+    },
     "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,
+      "license": "MIT",
       "engines": {
         "node": ">=10"
       },
@@ -373,9 +443,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.4.0",
@@ -430,9 +499,8 @@
     },
     "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,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "esrecurse": "^4.3.0",
         "estraverse": "^5.2.0"
@@ -446,9 +514,8 @@
     },
     "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,
+      "license": "Apache-2.0",
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
@@ -458,9 +525,8 @@
     },
     "node_modules/espree": {
       "version": "9.5.2",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
-      "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "acorn": "^8.8.0",
         "acorn-jsx": "^5.3.2",
@@ -475,9 +541,8 @@
     },
     "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,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "estraverse": "^5.1.0"
       },
@@ -487,9 +552,8 @@
     },
     "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,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "estraverse": "^5.2.0"
       },
@@ -499,18 +563,16 @@
     },
     "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,
+      "license": "BSD-2-Clause",
       "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,
+      "license": "BSD-2-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -521,36 +583,31 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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,
+      "license": "ISC",
       "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,
+      "license": "MIT",
       "dependencies": {
         "flat-cache": "^3.0.4"
       },
@@ -560,9 +617,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "locate-path": "^6.0.0",
         "path-exists": "^4.0.0"
@@ -576,9 +632,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "flatted": "^3.1.0",
         "rimraf": "^3.0.2"
@@ -587,23 +642,10 @@
         "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": {
+    "node_modules/flat-cache/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,
+      "license": "ISC",
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -619,11 +661,68 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/flat-cache/node_modules/rimraf": {
+      "version": "3.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.2.7",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/foreground-child": {
+      "version": "3.1.1",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/glob": {
+      "version": "10.3.14",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.6",
+        "minimatch": "^9.0.1",
+        "minipass": "^7.0.4",
+        "path-scurry": "^1.11.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "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,
+      "license": "ISC",
       "dependencies": {
         "is-glob": "^4.0.3"
       },
@@ -631,11 +730,30 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.4",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/globals": {
       "version": "13.20.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
-      "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "type-fest": "^0.20.2"
       },
@@ -648,33 +766,29 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "dependencies": {
         "parent-module": "^1.0.0",
         "resolve-from": "^4.0.0"
@@ -688,18 +802,16 @@
     },
     "node_modules/imurmurhash": {
       "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
       "dev": true,
+      "license": "MIT",
       "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,
+      "license": "ISC",
       "dependencies": {
         "once": "^1.3.0",
         "wrappy": "1"
@@ -707,31 +819,41 @@
     },
     "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
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/is-docker": {
-      "version": "2.0.0",
+      "version": "3.0.0",
       "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
       "engines": {
-        "node": ">=8"
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "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,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "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,
+      "license": "MIT",
       "dependencies": {
         "is-extglob": "^2.1.1"
       },
@@ -739,36 +861,67 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "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,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/is-wsl": {
-      "version": "2.2.0",
+      "version": "3.1.0",
       "license": "MIT",
       "dependencies": {
-        "is-docker": "^2.0.0"
+        "is-inside-container": "^1.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/isexe": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "2.3.6",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
     },
     "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,
+      "license": "MIT",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/js-sdsl"
@@ -776,9 +929,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "argparse": "^2.0.1"
       },
@@ -788,21 +940,18 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "prelude-ls": "^1.2.1",
         "type-check": "~0.4.0"
@@ -813,9 +962,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "p-locate": "^5.0.0"
       },
@@ -828,15 +976,20 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "10.2.2",
+      "license": "ISC",
+      "engines": {
+        "node": "14 || >=16.14"
+      }
     },
     "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,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -844,10 +997,16 @@
         "node": "*"
       }
     },
+    "node_modules/minipass": {
+      "version": "7.1.1",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/mkdirp": {
       "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
-      "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+      "license": "MIT",
       "bin": {
         "mkdirp": "dist/cjs/src/bin.js"
       },
@@ -860,19 +1019,29 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "2.1.11",
-      "license": "MIT"
+      "version": "5.0.7",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^18 || >=20"
+      }
     },
     "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
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/natural-orderby": {
       "version": "2.0.3",
@@ -882,30 +1051,42 @@
       }
     },
     "node_modules/node-fetch": {
-      "version": "2.6.0",
+      "version": "2.6.7",
       "license": "MIT",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
       "engines": {
         "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
       }
     },
     "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,
+      "license": "ISC",
       "dependencies": {
         "wrappy": "1"
       }
     },
     "node_modules/open": {
-      "version": "7.0.4",
+      "version": "10.1.0",
       "license": "MIT",
       "dependencies": {
-        "is-docker": "^2.0.0",
-        "is-wsl": "^2.1.1"
+        "default-browser": "^5.2.1",
+        "define-lazy-prop": "^3.0.0",
+        "is-inside-container": "^1.0.0",
+        "is-wsl": "^3.1.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -913,9 +1094,8 @@
     },
     "node_modules/optionator": {
       "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
-      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "deep-is": "^0.1.3",
         "fast-levenshtein": "^2.0.6",
@@ -930,9 +1110,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "yocto-queue": "^0.1.0"
       },
@@ -945,9 +1124,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "p-limit": "^3.0.2"
       },
@@ -960,9 +1138,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "callsites": "^3.0.0"
       },
@@ -972,53 +1149,59 @@
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
+    "node_modules/path-scurry": {
+      "version": "1.11.0",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "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": [
         {
@@ -1033,46 +1216,54 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ]
+      ],
+      "license": "MIT"
     },
     "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,
+      "license": "MIT",
       "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,
+      "license": "MIT",
       "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,
+      "version": "5.0.6",
+      "license": "ISC",
       "dependencies": {
-        "glob": "^7.1.3"
+        "glob": "^10.3.7"
       },
       "bin": {
-        "rimraf": "bin.js"
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=14"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/run-applescript": {
+      "version": "7.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "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": [
         {
@@ -1088,6 +1279,7 @@
           "url": "https://feross.org/support"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "queue-microtask": "^1.2.2"
       }
@@ -1101,9 +1293,7 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "shebang-regex": "^3.0.0"
       },
@@ -1113,30 +1303,108 @@
     },
     "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,
+      "license": "MIT",
       "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": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
+      "license": "MIT"
     },
     "node_modules/shortid": {
-      "version": "2.2.15",
-      "license": "MIT",
+      "version": "2.2.16",
+      "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz",
+      "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==",
+      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
       "dependencies": {
         "nanoid": "^2.1.0"
       }
     },
+    "node_modules/shortid/node_modules/nanoid": {
+      "version": "2.1.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
+      "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "license": "MIT"
+    },
+    "node_modules/string-width/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/string-width/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
     "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,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -1146,9 +1414,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,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       },
@@ -1158,9 +1425,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -1170,14 +1436,16 @@
     },
     "node_modules/temp-dir": {
       "version": "1.0.0",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
+      "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==",
       "engines": {
         "node": ">=4"
       }
     },
     "node_modules/tempy": {
       "version": "0.2.1",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.2.1.tgz",
+      "integrity": "sha512-LB83o9bfZGrntdqPuRdanIVCPReam9SOZKW0fOy5I9X3A854GGWi0tjCqoXEk84XIEYBc/x9Hq3EFop/H5wJaw==",
       "dependencies": {
         "temp-dir": "^1.0.0",
         "unique-string": "^1.0.0"
@@ -1188,9 +1456,12 @@
     },
     "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
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "license": "MIT"
     },
     "node_modules/truncate-utf8-bytes": {
       "version": "1.0.2",
@@ -1200,9 +1471,8 @@
       }
     },
     "node_modules/tui-lib": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/tui-lib/-/tui-lib-0.4.0.tgz",
-      "integrity": "sha512-P7PgQHWNK8yVlWZbWm7XLFwirkzQzKNYkhle2YYzj1Ba7fDuh5CITDLvogKFmZSC7RiBC4Y2+2uBpNcRAf1gwQ==",
+      "version": "0.4.1",
+      "license": "GPL-3.0",
       "dependencies": {
         "natural-orderby": "^3.0.2",
         "wcwidth": "^1.0.1"
@@ -1210,24 +1480,21 @@
     },
     "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==",
+      "license": "MIT",
       "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",
-      "integrity": "sha512-ySLdKfUHwxt6W1hub7Qt7smtuwujRHWxMIwdnO+IOzhd2B9naIg07JDr2LISZ3X+SZg0mvBNcGGeTf+L8bcSpw==",
+      "license": "GPL-3.0",
       "dependencies": {
         "tui-lib": "^0.1.1"
       }
     },
     "node_modules/tui-text-editor/node_modules/tui-lib": {
       "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/tui-lib/-/tui-lib-0.1.1.tgz",
-      "integrity": "sha512-QAE4axNCJ42IZSNnc2pLOkFtzHqYFgenDyw88JHHRNd8PXTVO8+JIpJArpgAguopd4MmoYaJbreze0BHoWMXfA==",
+      "license": "GPL-3.0",
       "dependencies": {
         "wcwidth": "^1.0.1",
         "word-wrap": "^1.2.3"
@@ -1235,9 +1502,8 @@
     },
     "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,
+      "license": "MIT",
       "dependencies": {
         "prelude-ls": "^1.2.1"
       },
@@ -1247,9 +1513,8 @@
     },
     "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,
+      "license": "(MIT OR CC0-1.0)",
       "engines": {
         "node": ">=10"
       },
@@ -1259,7 +1524,8 @@
     },
     "node_modules/unique-string": {
       "version": "1.0.0",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
+      "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==",
       "dependencies": {
         "crypto-random-string": "^1.0.0"
       },
@@ -1269,9 +1535,8 @@
     },
     "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,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "punycode": "^2.1.0"
       }
@@ -1287,11 +1552,21 @@
         "defaults": "^1.0.3"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "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,
+      "license": "ISC",
       "dependencies": {
         "isexe": "^2.0.0"
       },
@@ -1303,24 +1578,101 @@
       }
     },
     "node_modules/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==",
+      "version": "1.2.5",
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
     "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
+      "dev": true,
+      "license": "ISC"
     },
     "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,
+      "license": "MIT",
       "engines": {
         "node": ">=10"
       },
diff --git a/package.json b/package.json
index c1630a8..414b1f3 100644
--- a/package.json
+++ b/package.json
@@ -13,14 +13,15 @@
     "command-exists": "^1.2.9",
     "expand-home-dir": "0.0.3",
     "mkdirp": "^3.0.1",
+    "nanoid": "^5.0.7",
     "natural-orderby": "^2.0.3",
     "node-fetch": "^2.6.0",
-    "open": "^7.0.4",
+    "open": "^10.1.0",
+    "rimraf": "^5.0.6",
     "sanitize-filename": "^1.6.3",
     "shortid": "^2.2.15",
     "tempy": "^0.2.1",
     "shell-escape": "^0.2.0",
-    "tempy": "^0.2.1",
     "tui-lib": "^0.4.0",
     "tui-text-editor": "^0.3.1"
   },
diff --git a/playlist-utils.js b/playlist-utils.js
index dd1d8c8..eab0679 100644
--- a/playlist-utils.js
+++ b/playlist-utils.js
@@ -157,6 +157,31 @@ export function filterTracks(grouplike, handleTrack) {
   })
 }
 
+export function selectTracks(grouplike, mode = 'all', modeValue = 0) {
+  // Sort of like filterTracks, but with some ready-to-go modes for convenient
+  // results! Unlike filterTracks, this ignores the grouplike's structure and
+  // flattens it before processing and returning a flat list of tracks.
+
+  const all = flattenGrouplike(grouplike)
+
+  switch (mode) {
+    case 'all':
+      return all
+
+    case 'random': {
+      const count =
+        (modeValue.toString().endsWith('%')
+          ? Math.ceil(parseInt(modeValue) / 100 * all.items.length)
+          : parseInt(modeValue))
+
+      return {items: shuffleArray(all.items, count)}
+    }
+
+    default:
+      return {items: []}
+  }
+}
+
 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
diff --git a/tempdir.js b/tempdir.js
new file mode 100644
index 0000000..9264ffd
--- /dev/null
+++ b/tempdir.js
@@ -0,0 +1,86 @@
+import {mkdirSync, readdirSync, statSync} from 'node:fs'
+import * as os from 'node:os'
+import * as path from 'node:path'
+
+import {nanoid} from 'nanoid'
+import {rimrafSync} from 'rimraf'
+
+// Invariably obliterate contents of the rootDirectory upon
+// mtui startup if their mtime is older than this duration
+// before the current date.
+const ancient = 7 * 24 * 60 * 60 * 1000
+
+const ourTemporaryDirectories = []
+
+const rootDirectory = path.join(os.homedir(), '.mtui', 'tmp')
+
+function obliterateTemporaryDirectory(tempdir) {
+  const rel = path.relative(rootDirectory, tempdir)
+  if (rel.startsWith('/') || rel.startsWith('.')) {
+    console.trace()
+    console.error(`Ostensible tempdir located here:`)
+    console.error(tempdir)
+    console.error(`Doesn't appear to be located in tempdir root:`)
+    console.error(rootDirectory)
+    console.error(`This is a programming error, and possibly dangerous.`)
+    console.error(`So, exiting now. This should be investigated.`)
+    process.exit(1)
+  }
+
+  rimrafSync(tempdir)
+}
+
+function cleanupAncient() {
+  const fsOp = (fn, ...args) => {
+    try {
+      return fn(...args)
+    } catch (error) {
+      console.error(error)
+      console.error(`There was an error preparing the temporary file directory.`)
+      console.error(`You may be able to resolve this by deleting or moving away`)
+      console.error(`this path:`)
+      console.error(rootDirectory)
+      process.exit(1)
+    }
+  }
+
+  fsOp(() =>
+    mkdirSync(rootDirectory, {recursive: true}))
+
+  const tempdirs =
+    (fsOp(readdirSync, rootDirectory)
+      .map(dir => path.join(rootDirectory, dir)))
+
+  let first = true
+  for (const tempdir of tempdirs) {
+    const stats = fsOp(statSync, tempdir)
+
+    if (Date.now() - stats.mtimeMs > ancient) {
+      if (first) {
+        console.log(`One or more tempdirs haven't been modified in a while, removing:`)
+        first = false
+      }
+      console.log(tempdir)
+      fsOp(obliterateTemporaryDirectory, tempdir)
+    }
+  }
+}
+
+// This has to be a sync function, since it'll run on
+// the process' 'exit' event.
+function cleanupOurs() {
+  for (const tempdir of ourTemporaryDirectories) {
+    obliterateTemporaryDirectory(tempdir)
+  }
+}
+
+cleanupAncient()
+process.on('exit', cleanupOurs)
+
+export default function temporaryDirectory() {
+  const name = nanoid()
+  const dir = path.join(rootDirectory, name)
+  ourTemporaryDirectories.push(dir)
+  mkdirSync(dir)
+  return dir
+}
diff --git a/todo.txt b/todo.txt
index 63ba2e2..ac4807b 100644
--- a/todo.txt
+++ b/todo.txt
@@ -765,3 +765,9 @@ TODO: Use above socat system to keep "pinging" the socket until a response is
       through, we can avoid weirdness with commands being dropped beacuse they
       were sent too early. For now we just use a time-based delay on the base
       Socat class, which is a hack.
+
+TODO: When you're navigating down (or up) a menu, if that menu's got a
+      scrollbar *and* is divided into sections, passing a divider line should
+      try to scroll the whole newly active section into view! This way you get
+      all the context and don't miss out on realizing there are more items
+      below. (This only applies for keyboard navigation, not wheel scrolling.)
diff --git a/ui.js b/ui.js
index 6cabb32..cb38990 100644
--- a/ui.js
+++ b/ui.js
@@ -49,6 +49,7 @@ import {
   parentSymbol,
   reverseOrderOfGroups,
   searchForItem,
+  selectTracks,
   shuffleOrderOfGroups,
 } from './playlist-utils.js'
 
@@ -210,6 +211,8 @@ export default class AppElement extends FocusElement {
 
     this.timestampDictionary = new WeakMap()
 
+    this.itemMenuPage = 'cursor'
+
     // We add this is a child later (so that it's on top of every element).
     this.menuLayer = new DisplayElement()
     this.menuLayer.clickThrough = true
@@ -344,6 +347,15 @@ export default class AppElement extends FocusElement {
       {value: 'normal', label: 'In order'}
     ], this.showContextMenu)
 
+    this.selectGrouplikeItemsControl = new InlineListPickerElement('Which?', [
+      {value: 'all', label: 'all tracks'},
+      {value: '1', label: 'one track, randomly'},
+      {value: '4', label: 'four tracks, randomly'},
+      {value: '25%', label: '25% of the tracks, randomly'},
+      {value: '50%', label: '50% of the tracks, randomly'},
+      {value: '75%', label: '75% of the tracks, randomly'},
+    ], this.showContextMenu)
+
     this.menubar.buildItems([
       {text: 'mtui', menuItems: [
         {label: 'mtui (perpetual development)'},
@@ -980,32 +992,95 @@ export default class AppElement extends FocusElement {
     this.emitMarkChanged()
   }
 
-  markItem(item) {
-    if (isGroup(item)) {
-      for (const child of item.items) {
-        this.markItem(child)
+  selectTracksForMarking(item, forUnmarking = false, useGroupSelectionControl = false) {
+    let mode = 'all'
+    let modeValue = 0
+
+    if (useGroupSelectionControl) {
+      const value = this.selectGrouplikeItemsControl.curValue
+      if (value === 'all') {
+        // Do nothing, this is the default
+      } else if (value.endsWith('%')) {
+        mode = 'random'
+        modeValue = value
+      } else if (parseInt(value).toString() === value) {
+        mode = 'random'
+        modeValue = value
       }
+    }
+
+    const range = {
+      items:
+        (flattenGrouplike(item)
+          .items
+          .filter(isTrack)
+          .filter(track =>
+            this.markGrouplike.items.includes(track) === forUnmarking))
+    }
+
+    return selectTracks(range, mode, modeValue).items
+  }
+
+  markItem(item, useGroupSelectionControl = false) {
+    const { items: mark } = this.markGrouplike
+
+    let add
+    if (isGroup(item)) {
+      add = this.selectTracksForMarking(item, false, useGroupSelectionControl)
+    } else if (mark.includes(item)) {
+      add = []
     } else {
-      const { items } = this.markGrouplike
-      if (!items.includes(item)) {
-        items.push(item)
-        this.emitMarkChanged()
-      }
+      add = [item]
     }
+
+    if (!add.length) {
+      return
+    }
+
+    // If this is the first addition (starting from empty), switch the
+    // remembered context menu page so that the next context menu will show
+    // the marked items automatically.
+    if (!mark.length) {
+      this.itemMenuPage = 'mark'
+    }
+
+    for (const track of add) {
+      mark.push(track)
+    }
+
+    this.emitMarkChanged()
   }
 
-  unmarkItem(item) {
+  unmarkItem(item, useGroupSelectionControl) {
+    const { items: mark } = this.markGrouplike
+
+    let remove
     if (isGroup(item)) {
-      for (const child of item.items) {
-        this.unmarkItem(child)
-      }
+      remove = this.selectTracksForMarking(item, true, useGroupSelectionControl)
+    } else if (mark.includes(item)) {
+      remove = [item]
     } else {
-      const { items } = this.markGrouplike
-      if (items.includes(item)) {
-        items.splice(items.indexOf(item), 1)
-        this.emitMarkChanged()
-      }
+      remove = []
     }
+
+    if (!remove.length) {
+      return
+    }
+
+    for (const track of remove) {
+      mark.splice(mark.indexOf(track), 1)
+    }
+
+    // If this is the last removal (going to empty), switch the remembered
+    // context menu page so that the next context menu will show the usual
+    // controls for the item under the cursor. This isn't exactly necessary
+    // since various fallbacks will handle this value pointing to a page that
+    // doesn't exist anymore, but it's nice for consistency.
+    if (!mark.length) {
+      this.itemMenuPage = 'cursor'
+    }
+
+    this.emitMarkChanged()
   }
 
   getMarkStatus(item) {
@@ -1466,6 +1541,9 @@ export default class AppElement extends FocusElement {
         }
       }
 
+      // TODO: Implement this! :P
+      // const isMarked = false
+
       // const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt')
       const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing)
         ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)}
@@ -1519,50 +1597,66 @@ export default class AppElement extends FocusElement {
 
           ...((this.config.showPartyControls && !rootGroup.isPartySources)
             ? [
-              {label: 'Share with party', action: () => this.shareWithParty(item)},
-              {divider: true}
-            ]
+                {label: 'Share with party', action: () => this.shareWithParty(item)},
+                {divider: true}
+              ]
             : [
-              canControlQueue && isPlayable(item) && {element: this.whereControl},
-              canControlQueue && isGroup(item) && {element: this.orderControl},
-              canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)},
-              canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)},
-              {divider: true},
-              canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))},
-              canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))},
-              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)},
-              canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
-              isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)},
-              {divider: true},
-
-              timestampsItem,
-              ...(item === this.markGrouplike
-                ? [{label: 'Deselect all', action: () => this.unmarkAll()}]
-                : [
-                  this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item)},
-                  this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item)}
-                ])
-            ])
+                canControlQueue && isPlayable(item) && {element: this.whereControl},
+                canControlQueue && isGroup(item) && {element: this.orderControl},
+                canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)},
+                canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)},
+                {divider: true},
+                canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))},
+                canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))},
+                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)},
+                canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
+                isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)},
+                {divider: true},
+
+                timestampsItem,
+                ...(item === this.markGrouplike
+                  ? [{label: 'Deselect all', action: () => this.unmarkAll()}]
+                  : [
+                      isGroup(item) && {element: this.selectGrouplikeItemsControl},
+                      this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item)},
+                      this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item)},
+                    ])
+              ])
         ]
       }
     }
 
-    const pages = [
-      this.markGrouplike.items.length && generatePageForItem(this.markGrouplike),
-      el.item && generatePageForItem(el.item)
+    const pageInfo = [
+      this.markGrouplike.items.length && {
+        id: 'mark',
+        page: generatePageForItem(this.markGrouplike),
+      },
+
+      el.item && {
+        id: 'cursor',
+        page: generatePageForItem(el.item),
+      }
     ].filter(Boolean)
 
-    // TODO: Implement this! :P
-    // const isMarked = false
+    const pages = pageInfo.map(({ page }) => page)
 
-    this.showContextMenu({
+    let pageNum = pageInfo.findIndex(({ id }) => id === this.itemMenuPage)
+    if (pageNum === -1) pageNum = pageInfo.findIndex(({ id }) => id === 'cursor')
+    if (pageNum === -1) pageNum = 0
+
+    const menu = this.showContextMenu({
       x: el.absLeft,
       y: el.absTop + 1,
-      pages
+      pages,
+      pageNum,
+    })
+
+    menu.on('page changed', pageNum => {
+      this.itemMenuPage = pageInfo[pageNum].id
     })
   }
 
@@ -4906,6 +5000,7 @@ class ContextMenu extends FocusElement {
         }
         this.close(false)
         this.show({x, y, pages, pageNum})
+        this.emit('page changed', pageNum)
       }
     }
 
@@ -4917,6 +5012,7 @@ class ContextMenu extends FocusElement {
         }
         this.close(false)
         this.show({x, y, pages, pageNum})
+        this.emit('page changed', pageNum)
       }
     }
 
@@ -4928,7 +5024,7 @@ class ContextMenu extends FocusElement {
       if (pages.length === 0) {
         return
       }
-      itemsArg = pages[pageNum]
+      itemsArg = pages[Math.min(pages.length - 1, pageNum)]
     }
 
     let items = (typeof itemsArg === 'function') ? itemsArg() : itemsArg
@@ -5133,16 +5229,30 @@ class ContextMenu extends FocusElement {
 
     width += 2 // Space for the pane border
     height += 2 // Space for the pane border
-    if (this.form.scrollBarShown) width++
+
+    // Hilarity time: at THIS point, we can't identify if the form has its
+    // scroll bar visible, because we haven't constrained the height of the
+    // form. We're going to apply all the dimensions leading to the form twice,
+    // adapting the second time to be one wider if the form shows its scroll
+    // bar the first time.
+
     this.w = width
     this.h = height
-
     this.fitToParent()
 
     this.pane.fillParent()
     this.form.fillParent()
     this.form.fixLayout()
 
+    // We only need to update our own (and the descendants') width this time,
+    // because height won't have changed from the earlier measurement.
+    if (this.form.scrollBarShown) width++
+    this.w = width
+
+    this.pane.fillParent()
+    this.form.fillParent()
+    this.form.fixLayout()
+
     // After everything else, do a second pass to apply the decided width
     // to every element, so that they expand to all be the same width.
     // In order to change the width of a button (which is what these elements