diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | examples/basic-app.js | 10 | ||||
-rw-r--r-- | examples/command-line-interface.js (renamed from examples/interfacer-command-line.js) | 13 | ||||
-rw-r--r-- | examples/label.js | 5 | ||||
-rw-r--r-- | examples/list-scroll-form.js | 16 | ||||
-rw-r--r-- | examples/telnet-interface.js (renamed from examples/interfacer-telnet.js) | 23 | ||||
-rw-r--r-- | index.js | 44 | ||||
-rw-r--r-- | package-lock.json | 51 | ||||
-rw-r--r-- | package.json | 22 | ||||
-rw-r--r-- | ui/controls/Button.js (renamed from ui/form/Button.js) | 8 | ||||
-rw-r--r-- | ui/controls/FocusBox.js (renamed from ui/form/FocusBox.js) | 6 | ||||
-rw-r--r-- | ui/controls/Form.js (renamed from ui/form/Form.js) | 6 | ||||
-rw-r--r-- | ui/controls/ListScrollForm.js (renamed from ui/form/ListScrollForm.js) | 11 | ||||
-rw-r--r-- | ui/controls/ScrollBar.js (renamed from ui/form/ScrollBar.js) | 8 | ||||
-rw-r--r-- | ui/controls/TextInput.js (renamed from ui/form/TextInput.js) | 12 | ||||
-rw-r--r-- | ui/controls/index.js | 18 | ||||
-rw-r--r-- | ui/dialogs/CancelDialog.js (renamed from ui/form/CancelDialog.js) | 13 | ||||
-rw-r--r-- | ui/dialogs/ConfirmDialog.js (renamed from ui/form/ConfirmDialog.js) | 13 | ||||
-rw-r--r-- | ui/dialogs/Dialog.js (renamed from ui/Dialog.js) | 9 | ||||
-rw-r--r-- | ui/dialogs/FilePickerForm.js (renamed from ui/tools/FilePickerForm.js) | 23 | ||||
-rw-r--r-- | ui/dialogs/OpenFileDialog.js (renamed from ui/tools/OpenFileDialog.js) | 20 | ||||
-rw-r--r-- | ui/dialogs/index.js | 16 | ||||
-rw-r--r-- | ui/index.js | 4 | ||||
-rw-r--r-- | ui/presentation/HorizontalBox.js (renamed from ui/HorizontalBox.js) | 4 | ||||
-rw-r--r-- | ui/presentation/Label.js (renamed from ui/Label.js) | 6 | ||||
-rw-r--r-- | ui/presentation/Pane.js (renamed from ui/Pane.js) | 10 | ||||
-rw-r--r-- | ui/presentation/Sprite.js (renamed from ui/Sprite.js) | 6 | ||||
-rw-r--r-- | ui/presentation/WrapLabel.js (renamed from ui/WrapLabel.js) | 9 | ||||
-rw-r--r-- | ui/presentation/index.js | 15 | ||||
-rw-r--r-- | ui/primitives/DisplayElement.js (renamed from ui/DisplayElement.js) | 12 | ||||
-rw-r--r-- | ui/primitives/Element.js (renamed from ui/Element.js) | 7 | ||||
-rw-r--r-- | ui/primitives/FocusElement.js (renamed from ui/form/FocusElement.js) | 4 | ||||
-rw-r--r-- | ui/primitives/Root.js (renamed from ui/Root.js) | 26 | ||||
-rw-r--r-- | ui/primitives/index.js | 11 | ||||
-rw-r--r-- | util/ansi.js | 811 | ||||
-rw-r--r-- | util/count.js | 2 | ||||
-rw-r--r-- | util/exception.js | 2 | ||||
-rw-r--r-- | util/index.js | 10 | ||||
-rw-r--r-- | util/interfaces/CommandLineInterface.js (renamed from util/CommandLineInterfacer.js) | 9 | ||||
-rw-r--r-- | util/interfaces/Flushable.js (renamed from util/Flushable.js) | 6 | ||||
-rw-r--r-- | util/interfaces/TelnetInterface.js (renamed from util/TelnetInterfacer.js) | 9 | ||||
-rw-r--r-- | util/interfaces/index.js | 4 | ||||
-rw-r--r-- | util/smoothen.js | 2 | ||||
-rw-r--r-- | util/telchars.js | 2 | ||||
-rw-r--r-- | util/tui-app.js | 19 | ||||
-rw-r--r-- | util/unichars.js | 2 | ||||
-rw-r--r-- | util/waitForData.js | 2 | ||||
-rw-r--r-- | util/wrap.js | 28 |
48 files changed, 707 insertions, 663 deletions
diff --git a/README.md b/README.md index 4a7da5c..4952a8a 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,6 @@ My examples for you are the dumb pieces of code I've written with this: * [Knights & a Relatively Minimal Number of Dragons](https://github.com/towerofnix/KAaRMNoD/) * [DQ9 mapper](https://github.com/towerofnix/dq9-mapper) +* [mtui](https://nebula.ed1.club/git/mtui/) There are also some very, *very* minimalistic examples in [the examples folder](https://github.com/towerofnix/ui-lib/tree/master/examples). diff --git a/examples/basic-app.js b/examples/basic-app.js index bf8aa41..c4027d1 100644 --- a/examples/basic-app.js +++ b/examples/basic-app.js @@ -6,14 +6,14 @@ // - Sending a quit-app request via Control-C // // This script cannot actually be used on its own; see the examples on -// interfacers (interfacer-command-line.js and inerfacer-telnet.js) for a +// interfaces (command-line-interface.js and telnet-interface.js) for a // working demo. -const Pane = require('../ui/Pane') -const FocusElement = require('../ui/form/FocusElement') -const TextInput = require('../ui/form/TextInput') +import {TextInput} from 'tui-lib/ui/controls' +import {Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' -module.exports = class AppElement extends FocusElement { +export default class AppElement extends FocusElement { constructor() { super() diff --git a/examples/interfacer-command-line.js b/examples/command-line-interface.js index 1da6adf..ba1d936 100644 --- a/examples/interfacer-command-line.js +++ b/examples/command-line-interface.js @@ -1,11 +1,12 @@ -const Root = require('../ui/Root') -const CommandLineInterfacer = require('../util/CommandLineInterfacer') -const AppElement = require('./basic-app') +import {Root} from 'tui-lib/ui/primitives' +import {CommandLineInterface} from 'tui-lib/util/interfaces' -const interfacer = new CommandLineInterfacer() +import AppElement from './basic-app.js' -interfacer.getScreenSize().then(size => { - const root = new Root(interfacer) +const clInterface = new CommandLineInterface() + +clInterface.getScreenSize().then(size => { + const root = new Root(clInterface) root.w = size.width root.h = size.height diff --git a/examples/label.js b/examples/label.js index b8992d2..f9599c6 100644 --- a/examples/label.js +++ b/examples/label.js @@ -1,7 +1,8 @@ // An example of basic label usage. -const ansi = require('../util/ansi') -const Label = require('../ui/Label') +import {Label} from 'tui-lib/ui/presentation' + +import * as ansi from 'tui-lib/util/ansi' const label1 = new Label('Hello, world!') const label2 = new Label('I love labels.') diff --git a/examples/list-scroll-form.js b/examples/list-scroll-form.js index c015ddb..fc319a6 100644 --- a/examples/list-scroll-form.js +++ b/examples/list-scroll-form.js @@ -1,13 +1,13 @@ -const ansi = require('../util/ansi') -const Root = require('../ui/Root') -const CommandLineInterfacer = require('../util/CommandLineInterfacer') -const ListScrollForm = require('../ui/form/ListScrollForm') -const Button = require('../ui/form/Button') +import {Root} from 'tui-lib/ui/primitives' +import {Button, ListScrollForm} from 'tui-lib/ui/controls' -const interfacer = new CommandLineInterfacer() +import {CommandLineInterface} from 'tui-lib/util/interfaces' +import * as ansi from 'tui-lib/util/ansi' -interfacer.getScreenSize().then(size => { - const root = new Root(interfacer) +const clInterface = new CommandLineInterface() + +clInterface.getScreenSize().then(size => { + const root = new Root(clInterface) root.w = size.width root.h = size.height diff --git a/examples/interfacer-telnet.js b/examples/telnet-interface.js index d7aad43..319786f 100644 --- a/examples/interfacer-telnet.js +++ b/examples/telnet-interface.js @@ -1,23 +1,26 @@ // Telnet demo: -// - Basic telnet socket handling using the TelnetInterfacer +// - Basic telnet socket handling using the TelnetInterface // - Handling client's screen size // - Handling socket being closed by client // - Handling cleanly closing the socket by hand -const net = require('net') -const Root = require('../ui/Root') -const TelnetInterfacer = require('../TelnetInterfacer') -const AppElement = require('./basic-app') +import net from 'node:net' + +import {Root} from 'tui-lib/ui/primitives' + +import {TelnetInterface} from 'tui-lib/util/interfaces' + +import AppElement from './basic-app.js' const server = new net.Server(socket => { - const interfacer = new TelnetInterfacer(socket) + const telnetInterface = new TelnetInterface(socket) - interfacer.getScreenSize().then(size => { - const root = new Root(interfacer) + telnetInterface.getScreenSize().then(size => { + const root = new Root(telnetInterface) root.w = size.width root.h = size.height - interfacer.on('resize', newSize => { + telnetInterface.on('resize', newSize => { root.w = newSize.width root.h = newSize.height root.fixAllLayout() @@ -31,7 +34,7 @@ const server = new net.Server(socket => { appElement.on('quitRequested', () => { if (!closed) { - interfacer.cleanTelnetOptions() + telnetInterface.cleanTelnetOptions() socket.write('Goodbye!\n') socket.end() clearInterval(interval) diff --git a/index.js b/index.js index b848814..fc54bfd 100644 --- a/index.js +++ b/index.js @@ -1,37 +1,7 @@ -module.exports = { - ui: { - Dialog: require('./ui/Dialog'), - DisplayElement: require('./ui/DisplayElement'), - HorizontalBox: require('./ui/HorizontalBox'), - Label: require('./ui/Label'), - Pane: require('./ui/Pane'), - Root: require('./ui/Root'), - Sprite: require('./ui/Sprite'), - WrapLabel: require('./ui/WrapLabel'), - form: { - Button: require('./ui/form/Button'), - CancelDialog: require('./ui/form/CancelDialog'), - ConfirmDialog: require('./ui/form/ConfirmDialog'), - FocusBox: require('./ui/form/FocusBox'), - FocusElement: require('./ui/form/FocusElement'), - Form: require('./ui/form/Form'), - ListScrollForm: require('./ui/form/ListScrollForm'), - ScrollBar: require('./ui/form/ScrollBar'), - TextInput: require('./ui/form/TextInput') - } - }, - util: { - tuiApp: require('./util/tui-app'), - ansi: require('./util/ansi'), - CommandLineInterfacer: require('./util/CommandLineInterfacer'), - count: require('./util/count'), - exception: require('./util/exception'), - Flushable: require('./util/Flushable'), - smoothen: require('./util/smoothen'), - telchars: require('./util/telchars'), - TelnetInterfacer: require('./util/TelnetInterfacer'), - unichars: require('./util/unichars'), - waitForData: require('./util/waitForData'), - wrap: require('./util/wrap') - } -} +export * as ui from './ui/index.js' +export * as util from './util/index.js' + +import * as ui from './ui/index.js' +import * as util from './util/index.js' + +export default {ui, util} diff --git a/package-lock.json b/package-lock.json index e62e5f9..fd6d714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,61 +1,50 @@ { "name": "tui-lib", - "version": "0.3.3", - "lockfileVersion": 2, + "version": "0.4.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tui-lib", - "version": "0.3.3", + "version": "0.4.0", "license": "GPL-3.0", "dependencies": { + "natural-orderby": "^3.0.2", "wcwidth": "^1.0.1" } }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "engines": { "node": ">=0.8" } }, "node_modules/defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dependencies": { "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dependencies": { - "defaults": "^1.0.3" - } - } - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "requires": { - "clone": "^1.0.2" + "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" } }, - "wcwidth": { + "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "requires": { + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { "defaults": "^1.0.3" } } diff --git a/package.json b/package.json index f07f4d1..4816f36 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,26 @@ { "name": "tui-lib", - "version": "0.3.3", + "version": "0.4.0", "description": "terminal ui library", - "main": "index.js", - "repository": "https://notabug.org/towerofnix/tui-lib.git", - "author": "Florrie <towerofnix@gmail.com>", + "type": "module", + "repository": "https://nebula.ed1.club/git/tui-lib/", + "author": "Nebula <qznebula@protonmail.com>", "license": "GPL-3.0", "dependencies": { + "natural-orderby": "^3.0.2", "wcwidth": "^1.0.1" + }, + "exports": { + ".": "./index.js", + "./ui": "./ui/index.js", + "./ui/controls": "./ui/controls/index.js", + "./ui/dialogs": "./ui/dialogs/index.js", + "./ui/presentation": "./ui/presentation/index.js", + "./ui/primitives": "./ui/primitives/index.js", + "./util/ansi": "./util/ansi.js", + "./util/exception": "./util/exception.js", + "./util/interfaces": "./util/interfaces/index.js", + "./util/telchars": "./util/telchars.js", + "./util/unichars": "./util/unichars.js" } } diff --git a/ui/form/Button.js b/ui/controls/Button.js index 46329a6..5be2b2a 100644 --- a/ui/form/Button.js +++ b/ui/controls/Button.js @@ -1,9 +1,9 @@ -const ansi = require('../../util/ansi') -const telc = require('../../util/telchars') +import {FocusElement} from 'tui-lib/ui/primitives' -const FocusElement = require('./FocusElement') +import * as ansi from 'tui-lib/util/ansi' +import telc from 'tui-lib/util/telchars' -module.exports = class Button extends FocusElement { +export default class Button extends FocusElement { // A button. constructor(text) { diff --git a/ui/form/FocusBox.js b/ui/controls/FocusBox.js index 69b5bf5..64f84c9 100644 --- a/ui/form/FocusBox.js +++ b/ui/controls/FocusBox.js @@ -1,8 +1,8 @@ -const ansi = require('../../util/ansi') +import {FocusElement} from 'tui-lib/ui/primitives' -const FocusElement = require('./FocusElement') +import * as ansi from 'tui-lib/util/ansi' -module.exports = class FocusBox extends FocusElement { +export default class FocusBox extends FocusElement { // A box (not to be confused with Pane!) that can be selected. When it's // selected, it applies an invert effect to its children. (This won't work // well if you have elements inside of it that have their own attributes, diff --git a/ui/form/Form.js b/ui/controls/Form.js index f61c7b6..0224247 100644 --- a/ui/form/Form.js +++ b/ui/controls/Form.js @@ -1,8 +1,8 @@ -const telc = require('../../util/telchars') +import telc from 'tui-lib/util/telchars' -const FocusElement = require('./FocusElement') +import {FocusElement} from 'tui-lib/ui/primitives' -module.exports = class Form extends FocusElement { +export default class Form extends FocusElement { constructor() { super() diff --git a/ui/form/ListScrollForm.js b/ui/controls/ListScrollForm.js index e4f4249..f74561e 100644 --- a/ui/form/ListScrollForm.js +++ b/ui/controls/ListScrollForm.js @@ -1,10 +1,11 @@ -const ansi = require('../../util/ansi') -const telc = require('../../util/telchars') +import * as ansi from 'tui-lib/util/ansi' +import telc from 'tui-lib/util/telchars' +import unic from 'tui-lib/util/unichars' -const Form = require('./Form') -const ScrollBar = require('./ScrollBar') +import Form from './Form.js' +import ScrollBar from './ScrollBar.js' -module.exports = class ListScrollForm extends Form { +export default class ListScrollForm extends Form { // A form that lets the user scroll through a list of items. It // automatically adjusts to always allow the selected item to be visible. // Unless disabled in the constructor, a scrollbar is automatically displayed diff --git a/ui/form/ScrollBar.js b/ui/controls/ScrollBar.js index 13ba7fe..4b79d57 100644 --- a/ui/form/ScrollBar.js +++ b/ui/controls/ScrollBar.js @@ -1,9 +1,9 @@ -const DisplayElement = require('../DisplayElement') +import * as ansi from 'tui-lib/util/ansi' +import unic from 'tui-lib/util/unichars' -const ansi = require('../../util/ansi') -const unic = require('../../util/unichars') +import {DisplayElement} from 'tui-lib/ui/primitives' -module.exports = class ScrollBar extends DisplayElement { +export default class ScrollBar extends DisplayElement { constructor({ getLayoutType, getCurrentScroll, diff --git a/ui/form/TextInput.js b/ui/controls/TextInput.js index 78d3b6d..1a32605 100644 --- a/ui/form/TextInput.js +++ b/ui/controls/TextInput.js @@ -1,10 +1,10 @@ -const ansi = require('../../util/ansi') -const unic = require('../../util/unichars') -const telc = require('../../util/telchars') +import {FocusElement} from 'tui-lib/ui/primitives' -const FocusElement = require('./FocusElement') +import * as ansi from 'tui-lib/util/ansi' +import telc from 'tui-lib/util/telchars' +import unic from 'tui-lib/util/unichars' -module.exports = class TextInput extends FocusElement { +export default class TextInput extends FocusElement { // An element that the user can type in. constructor() { @@ -142,4 +142,6 @@ module.exports = class TextInput extends FocusElement { get value() { return this.getDep('value') } set value(v) { return this.setDep('value', v) } + get cursorIndex() { return this.getDep('cursorIndex') } + set cursorIndex(v) { return this.setDep('cursorIndex', v) } } diff --git a/ui/controls/index.js b/ui/controls/index.js new file mode 100644 index 0000000..7f290c2 --- /dev/null +++ b/ui/controls/index.js @@ -0,0 +1,18 @@ +// +// Import mapping: +// +// primitives -> +// Button +// FocusBox +// ScrollBar +// TextInput +// +// Form -> ListScrollForm +// + +export {default as Button} from './Button.js' +export {default as ScrollBar} from './ScrollBar.js' +export {default as FocusBox} from './FocusBox.js' +export {default as Form} from './Form.js' +export {default as ListScrollForm} from './ListScrollForm.js' +export {default as TextInput} from './TextInput.js' diff --git a/ui/form/CancelDialog.js b/ui/dialogs/CancelDialog.js index 21ff6df..9069d43 100644 --- a/ui/form/CancelDialog.js +++ b/ui/dialogs/CancelDialog.js @@ -1,13 +1,10 @@ -const telc = require('../../util/telchars') +import {Button, Form} from 'tui-lib/ui/controls' +import {Label, Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' -const FocusElement = require('./FocusElement') +import telc from 'tui-lib/util/telchars' -const Button = require('./Button') -const Form = require('./Form') -const Label = require('../Label') -const Pane = require('../Pane') - -module.exports = class ConfirmDialog extends FocusElement { +export default class CancelDialog extends FocusElement { // A basic cancel dialog. Has one buttons, cancel, and a label. // The escape (esc) key can be used to exit the dialog (which sends a // 'cancelled' event, as the cancel button also does). diff --git a/ui/form/ConfirmDialog.js b/ui/dialogs/ConfirmDialog.js index 230230d..c0bcfae 100644 --- a/ui/form/ConfirmDialog.js +++ b/ui/dialogs/ConfirmDialog.js @@ -1,13 +1,10 @@ -const telc = require('../../util/telchars') +import {Button, Form} from 'tui-lib/ui/controls' +import {Label, Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' -const FocusElement = require('./FocusElement') +import telc from 'tui-lib/util/telchars' -const Button = require('./Button') -const Form = require('./Form') -const Label = require('../Label') -const Pane = require('../Pane') - -module.exports = class ConfirmDialog extends FocusElement { +export default class ConfirmDialog extends FocusElement { // A basic yes/no dialog. Has two buttons, confirm/cancel, and a label. // The escape (esc) key can be used to exit the dialog (which sends a // 'cancelled' event, as the cancel button also does). diff --git a/ui/Dialog.js b/ui/dialogs/Dialog.js index 0b77b12..19565f5 100644 --- a/ui/Dialog.js +++ b/ui/dialogs/Dialog.js @@ -1,10 +1,9 @@ -const FocusElement = require('./form/FocusElement') +import {Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' -const Pane = require('./Pane') +import telc from 'tui-lib/util/telchars' -const telc = require('../util/telchars') - -module.exports = class Dialog extends FocusElement { +export default class Dialog extends FocusElement { // A simple base dialog. // // Emits the 'cancelled' event when the cancel key (escape) is pressed, diff --git a/ui/tools/FilePickerForm.js b/ui/dialogs/FilePickerForm.js index 51d59a9..6414818 100644 --- a/ui/tools/FilePickerForm.js +++ b/ui/dialogs/FilePickerForm.js @@ -1,15 +1,11 @@ -const fs = require('fs') -const util = require('util') -const path = require('path') +import {readdir, stat} from 'node:fs/promises' +import path from 'node:path' -const readdir = util.promisify(fs.readdir) -const stat = util.promisify(fs.stat) -const naturalSort = require('node-natural-sort') +import {compare as naturalCompare} from 'natural-orderby' -const Button = require('../form/Button') -const ListScrollForm = require('../form/ListScrollForm') +import {Button, ListScrollForm} from 'tui-lib/ui/controls' -module.exports = class FilePickerForm extends ListScrollForm { +export default class FilePickerForm extends ListScrollForm { fillItems(dirPath) { this.inputs = [] this.children = [] @@ -33,14 +29,10 @@ module.exports = class FilePickerForm extends ListScrollForm { }) })) - const sort = naturalSort({ - properties: { - caseSensitive: false - } - }) + const compare = naturalCompare() processedItems.sort((a, b) => { if (a.isDirectory === b.isDirectory) { - return sort(a.label, b.label) + return compare(a.label, b.label) } else { if (a.isDirectory) { return -1 @@ -85,4 +77,3 @@ module.exports = class FilePickerForm extends ListScrollForm { }) } } - diff --git a/ui/tools/OpenFileDialog.js b/ui/dialogs/OpenFileDialog.js index 43f2638..970e291 100644 --- a/ui/tools/OpenFileDialog.js +++ b/ui/dialogs/OpenFileDialog.js @@ -1,13 +1,12 @@ -const path = require('path') +import path from 'node:path' -const Button = require('../form/Button') -const Dialog = require('../Dialog') -const FilePickerForm = require('./FilePickerForm') -const Form = require('../form/Form') -const Label = require('../Label') -const TextInput = require('../form/TextInput') +import {Button, Form, TextInput} from 'tui-lib/ui/controls' +import {Label} from 'tui-lib/ui/presentation' -module.exports = class OpenFileDialog extends Dialog { +import Dialog from './Dialog.js' +import FilePickerForm from './FilePickerForm.js' + +export default class OpenFileDialog extends Dialog { constructor() { super() @@ -42,7 +41,7 @@ module.exports = class OpenFileDialog extends Dialog { { const cb = append => p => { - this.filePathInput.setValue((path.relative(__dirname, p) || '.') + append) + this.filePathInput.setValue((path.relative(process.cwd(), p) || '.') + append) } this.filePickerForm.on('selected', cb('')) @@ -54,7 +53,7 @@ module.exports = class OpenFileDialog extends Dialog { }) const dir = (this.lastFilePath - ? path.relative(__dirname, path.dirname(this.lastFilePath)) + '/' + ? path.relative(process.cwd(), path.dirname(this.lastFilePath)) + '/' : './') this.filePathInput.setValue(dir) @@ -107,4 +106,3 @@ module.exports = class OpenFileDialog extends Dialog { }) } } - diff --git a/ui/dialogs/index.js b/ui/dialogs/index.js new file mode 100644 index 0000000..5cb9f04 --- /dev/null +++ b/ui/dialogs/index.js @@ -0,0 +1,16 @@ +// +// Import mapping: +// +// controls, presentation, primitives -> +// CancelDialog +// ConfirmDialog +// +// Dialog -> OpenFileDialog +// FilePickerForm -> OpenFileDialog +// + +export {default as CancelDialog} from './CancelDialog.js' +export {default as ConfirmDialog} from './ConfirmDialog.js' +export {default as Dialog} from './Dialog.js' +export {default as FilePickerForm} from './FilePickerForm.js' +export {default as OpenFileDialog} from './OpenFileDialog.js' diff --git a/ui/index.js b/ui/index.js new file mode 100644 index 0000000..df6cae8 --- /dev/null +++ b/ui/index.js @@ -0,0 +1,4 @@ +export * as controls from './controls/index.js' +export * as dialogs from './dialogs/index.js' +export * as presentation from './presentation/index.js' +export * as primitives from './primitives/index.js' diff --git a/ui/HorizontalBox.js b/ui/presentation/HorizontalBox.js index f92bf10..d396ec3 100644 --- a/ui/HorizontalBox.js +++ b/ui/presentation/HorizontalBox.js @@ -1,6 +1,6 @@ -const DisplayElement = require('./DisplayElement') +import {DisplayElement} from 'tui-lib/ui/primitives' -module.exports = class HorizontalBox extends DisplayElement { +export default class HorizontalBox extends DisplayElement { // A box that will automatically lay out its children in a horizontal row. fixLayout() { diff --git a/ui/Label.js b/ui/presentation/Label.js index b5828cb..ed45601 100644 --- a/ui/Label.js +++ b/ui/presentation/Label.js @@ -1,8 +1,8 @@ -const ansi = require('../util/ansi') +import {DisplayElement} from 'tui-lib/ui/primitives' -const DisplayElement = require('./DisplayElement') +import * as ansi from 'tui-lib/util/ansi' -module.exports = class Label extends DisplayElement { +export default class Label extends DisplayElement { // A simple text display. Automatically adjusts size to fit text. // // Supports formatted text in two ways: diff --git a/ui/Pane.js b/ui/presentation/Pane.js index b33a1b7..4769cf9 100644 --- a/ui/Pane.js +++ b/ui/presentation/Pane.js @@ -1,11 +1,11 @@ -const ansi = require('../util/ansi') -const unic = require('../util/unichars') +import {DisplayElement} from 'tui-lib/ui/primitives' -const DisplayElement = require('./DisplayElement') +import * as ansi from 'tui-lib/util/ansi' +import unic from 'tui-lib/util/unichars' -const Label = require('./Label') +import Label from './Label.js' -module.exports = class Pane extends DisplayElement { +export default class Pane extends DisplayElement { // A simple rectangular framed pane. constructor() { diff --git a/ui/Sprite.js b/ui/presentation/Sprite.js index 701f1b8..49ee450 100644 --- a/ui/Sprite.js +++ b/ui/presentation/Sprite.js @@ -1,8 +1,8 @@ -const ansi = require('../util/ansi') +import {DisplayElement} from 'tui-lib/ui/primitives' -const DisplayElement = require('./DisplayElement') +import * as ansi from 'tui-lib/util/ansi' -module.exports = class Sprite extends DisplayElement { +export default class Sprite extends DisplayElement { // "A sprite is a two-dimensional bitmap that is integrated into a larger // scene." - Wikipedia // diff --git a/ui/WrapLabel.js b/ui/presentation/WrapLabel.js index d621a49..eae8960 100644 --- a/ui/WrapLabel.js +++ b/ui/presentation/WrapLabel.js @@ -1,9 +1,8 @@ -const ansi = require('../util/ansi') -const wrap = require('../util/wrap') +import * as ansi from 'tui-lib/util/ansi' -const Label = require('./Label') +import Label from './Label.js' -module.exports = class WrapLabel extends Label { +export default class WrapLabel extends Label { // A word-wrapping text display. Given a width, wraps text to fit. constructor(...args) { @@ -29,7 +28,7 @@ module.exports = class WrapLabel extends Label { return [] } - return wrap(this.text, this.w - 1).map(l => l.trim()) + return ansi.wrapToColumns(this.text, this.w - 1).map(l => l.trim()) } get h() { diff --git a/ui/presentation/index.js b/ui/presentation/index.js new file mode 100644 index 0000000..9605d25 --- /dev/null +++ b/ui/presentation/index.js @@ -0,0 +1,15 @@ +// +// Import mapping: +// +// primitives -> +// HorizontalBox +// Sprite +// +// Label -> Pane, WrapLabel +// + +export {default as HorizontalBox} from './HorizontalBox.js' +export {default as Label} from './Label.js' +export {default as Pane} from './Pane.js' +export {default as Sprite} from './Sprite.js' +export {default as WrapLabel} from './WrapLabel.js' diff --git a/ui/DisplayElement.js b/ui/primitives/DisplayElement.js index 32a62b8..6452887 100644 --- a/ui/DisplayElement.js +++ b/ui/primitives/DisplayElement.js @@ -1,6 +1,6 @@ -const Element = require('./Element') +import Element from './Element.js' -module.exports = class DisplayElement extends Element { +export default class DisplayElement extends Element { // A general class that handles dealing with screen coordinates, the tree // of elements, and other common stuff. // @@ -10,6 +10,10 @@ module.exports = class DisplayElement extends Element { // It's a subclass of EventEmitter, so you can make your own events within // the logic of your subclass. + static drawValues = Symbol('drawValues') + static lastDrawValues = Symbol('lastDrawValues') + static scheduledDraw = Symbol('scheduledDraw') + constructor() { super() @@ -303,7 +307,3 @@ module.exports = class DisplayElement extends Element { get absTop() { return this.absY } get absBottom() { return this.absY + this.h - 1 } } - -module.exports.drawValues = Symbol('drawValues') -module.exports.lastDrawValues = Symbol('lastDrawValues') -module.exports.scheduledDraw = Symbol('scheduledDraw') diff --git a/ui/Element.js b/ui/primitives/Element.js index b9b8c61..a5dbea6 100644 --- a/ui/Element.js +++ b/ui/primitives/Element.js @@ -1,7 +1,8 @@ -const EventEmitter = require('events') -const exception = require('../util/exception') +import EventEmitter from 'node:events' -module.exports = class Element extends EventEmitter { +import exception from 'tui-lib/util/exception' + +export default class Element extends EventEmitter { // The basic class containing methods for working with an element hierarchy. // Generally speaking, you usually want to extend DisplayElement instead of // this class. diff --git a/ui/form/FocusElement.js b/ui/primitives/FocusElement.js index 23c2e02..2c23b1e 100644 --- a/ui/form/FocusElement.js +++ b/ui/primitives/FocusElement.js @@ -1,6 +1,6 @@ -const DisplayElement = require('../DisplayElement') +import DisplayElement from './DisplayElement.js' -module.exports = class FocusElement extends DisplayElement { +export default class FocusElement extends DisplayElement { // A basic element that can receive cursor focus. constructor() { diff --git a/ui/Root.js b/ui/primitives/Root.js index 2b13203..a779637 100644 --- a/ui/Root.js +++ b/ui/primitives/Root.js @@ -1,19 +1,17 @@ -const ansi = require('../util/ansi') -const telc = require('../util/telchars') +import * as ansi from 'tui-lib/util/ansi' +import telc from 'tui-lib/util/telchars' -const DisplayElement = require('./DisplayElement') +import DisplayElement from './DisplayElement.js' -const Form = require('./form/Form') - -module.exports = class Root extends DisplayElement { +export default class Root extends DisplayElement { // An element to be used as the root of a UI. Handles lots of UI and // socket stuff. - constructor(interfacer, writable = null) { + constructor(interfaceArg, writable = null) { super() - this.interfacer = interfacer - this.writable = writable || interfacer + this.interface = interfaceArg + this.writable = writable || interfaceArg this.selectedElement = null @@ -21,7 +19,7 @@ module.exports = class Root extends DisplayElement { this.oldSelectionStates = [] - interfacer.on('inputData', buf => this.handleData(buf)) + this.interface.on('inputData', buf => this.handleData(buf)) this.renderCount = 0 } @@ -174,11 +172,17 @@ module.exports = class Root extends DisplayElement { // If the element is part of a form, just be lazy and pass control to that // form...unless the form itself asked us to select the element! + // // TODO: This is so that if an element is selected, its parent form will // automatically see that and correctly update its curIndex... but what if // the element is an input of a form which is NOT its parent? + // + // XXX: We currently use a HUGE HACK instead of `instanceof` to avoid + // breaking the rule of import direction (controls -> primitives, never + // the other way around). This is bad for obvious reasons, but I haven't + // yet looked into what the correct approach would be. const parent = el.parent - if (!fromForm && parent instanceof Form && parent.inputs.includes(el)) { + if (!fromForm && parent.constructor.name === 'Form' && parent.inputs.includes(el)) { parent.selectInput(el) return } diff --git a/ui/primitives/index.js b/ui/primitives/index.js new file mode 100644 index 0000000..4e36452 --- /dev/null +++ b/ui/primitives/index.js @@ -0,0 +1,11 @@ +// +// Import mapping: +// +// Element -> +// DisplayElement -> FocusElement, Root +// + +export {default as DisplayElement} from './DisplayElement.js' +export {default as Element} from './Element.js' +export {default as FocusElement} from './FocusElement.js' +export {default as Root} from './Root.js' diff --git a/util/ansi.js b/util/ansi.js index d4ed71d..4e8abb0 100644 --- a/util/ansi.js +++ b/util/ansi.js @@ -1,503 +1,528 @@ -const wcwidth = require('wcwidth') +import wcwidth from 'wcwidth' -const ESC = '\x1b' - -const isDigit = char => '0123456789'.indexOf(char) >= 0 +function isDigit(char) { + return '0123456789'.indexOf(char) >= 0 +} -const ansi = { - ESC, +export const ESC = '\x1b' + +// Attributes +export const A_RESET = 0 +export const A_BRIGHT = 1 +export const A_DIM = 2 +export const A_INVERT = 7 + +// Colors +export const C_BLACK = 30 +export const C_RED = 31 +export const C_GREEN = 32 +export const C_YELLOW = 33 +export const C_BLUE = 34 +export const C_MAGENTA = 35 +export const C_CYAN = 36 +export const C_WHITE = 37 +export const C_RESET = 39 + +export function clearScreen() { + // Clears the screen, removing any characters displayed, and resets the + // cursor position. + + return `${ESC}[2J` +} - // Attributes - A_RESET: 0, - A_BRIGHT: 1, - A_DIM: 2, - A_INVERT: 7, - C_BLACK: 30, - C_RED: 31, - C_GREEN: 32, - C_YELLOW: 33, - C_BLUE: 34, - C_MAGENTA: 35, - C_CYAN: 36, - C_WHITE: 37, - C_RESET: 39, +export function moveCursorRaw(line, col) { + // Moves the cursor to the given line and column on the screen. + // Returns the pure ANSI code, with no modification to line or col. - clearScreen() { - // Clears the screen, removing any characters displayed, and resets the - // cursor position. + return `${ESC}[${line};${col}H` +} - return `${ESC}[2J` - }, +export function moveCursor(line, col) { + // Moves the cursor to the given line and column on the screen. + // Note that since in JavaScript indexes start at 0, but in ANSI codes + // the top left of the screen is (1, 1), this function adjusts the + // arguments to act as if the top left of the screen is (0, 0). - moveCursorRaw(line, col) { - // Moves the cursor to the given line and column on the screen. - // Returns the pure ANSI code, with no modification to line or col. + return `${ESC}[${line + 1};${col + 1}H` +} - return `${ESC}[${line};${col}H` - }, +export function cleanCursor() { + // A combination of codes that generally cleans up the cursor. - moveCursor(line, col) { - // Moves the cursor to the given line and column on the screen. - // Note that since in JavaScript indexes start at 0, but in ANSI codes - // the top left of the screen is (1, 1), this function adjusts the - // arguments to act as if the top left of the screen is (0, 0). + return resetAttributes() + + stopTrackingMouse() + + showCursor() +} - return `${ESC}[${line + 1};${col + 1}H` - }, +export function hideCursor() { + // Makes the cursor invisible. - cleanCursor() { - // A combination of codes that generally cleans up the cursor. + return `${ESC}[?25l` +} - return ansi.resetAttributes() + - ansi.stopTrackingMouse() + - ansi.showCursor() - }, +export function showCursor() { + // Makes the cursor visible. - hideCursor() { - // Makes the cursor invisible. + return `${ESC}[?25h` +} - return `${ESC}[?25l` - }, +export function resetAttributes() { + // Resets all attributes, including text decorations, foreground and + // background color. - showCursor() { - // Makes the cursor visible. + return `${ESC}[0m` +} - return `${ESC}[?25h` - }, +export function setAttributes(attrs) { + // Set some raw attributes. See the attributes section of the ansi.js + // source code for attributes that can be used with this; A_RESET resets + // all attributes. - resetAttributes() { - // Resets all attributes, including text decorations, foreground and - // background color. + return `${ESC}[${attrs.join(';')}m` +} - return `${ESC}[0m` - }, +export function setForeground(color) { + // Sets the foreground color to print text with. See C_(COLOR) for colors + // that can be used with this; C_RESET resets the foreground. + // + // If null or undefined is passed, this function will return a blank + // string (no ANSI escape codes). - setAttributes(attrs) { - // Set some raw attributes. See the attributes section of the ansi.js - // source code for attributes that can be used with this; A_RESET resets - // all attributes. + if (typeof color === 'undefined' || color === null) { + return '' + } - return `${ESC}[${attrs.join(';')}m` - }, + return setAttributes([color]) +} - setForeground(color) { - // Sets the foreground color to print text with. See C_(COLOR) for colors - // that can be used with this; C_RESET resets the foreground. - // - // If null or undefined is passed, this function will return a blank - // string (no ANSI escape codes). +export function setBackground(color) { + // Sets the background color to print text with. Accepts the same arguments + // as setForeground (C_(COLOR), C_RESET, etc). + // + // Note that attributes such as A_BRIGHT and A_DIM apply apply to only the + // foreground, not the background. To set a bright or dim background, you + // can set the appropriate color as the foreground and then invert. - if (typeof color === 'undefined' || color === null) { - return '' - } + if (typeof color === 'undefined' || color === null) { + return '' + } - return ansi.setAttributes([color]) - }, + return setAttributes([color + 10]) +} - setBackground(color) { - // Sets the background color to print text with. Accepts the same arguments - // as setForeground (C_(COLOR), C_RESET, etc). - // - // Note that attributes such as A_BRIGHT and A_DIM apply apply to only the - // foreground, not the background. To set a bright or dim background, you - // can set the appropriate color as the foreground and then invert. +export function invert() { + // Inverts the foreground and background colors. - if (typeof color === 'undefined' || color === null) { - return '' - } + return `${ESC}[7m` +} - return ansi.setAttributes([color + 10]) - }, +export function invertOff() { + // Un-inverts the foreground and backgrund colors. - invert() { - // Inverts the foreground and background colors. + return `${ESC}[27m` +} - return `${ESC}[7m` - }, +export function startTrackingMouse() { + return `${ESC}[?1002h` +} - invertOff() { - // Un-inverts the foreground and backgrund colors. +export function stopTrackingMouse() { + return `${ESC}[?1002l` +} - return `${ESC}[27m` - }, +export function requestCursorPosition() { + // Requests the position of the cursor. + // Expect a stdin-result '\ESC[l;cR', where l is the line number (1-based), + // c is the column number (also 1-based), and R is the literal character + // 'R' (decimal code 82). - startTrackingMouse() { - return `${ESC}[?1002h` - }, + return `${ESC}[6n` +} - stopTrackingMouse() { - return `${ESC}[?1002l` - }, +export function enableAlternateScreen() { + // Enables alternate screen: + // "Xterm maintains two screen buffers. The normal screen buffer allows + // you to scroll back to view saved lines of output up to the maximum set + // by the saveLines resource. The alternate screen buffer is exactly as + // large as the display, contains no additional saved lines." - requestCursorPosition() { - // Requests the position of the cursor. - // Expect a stdin-result '\ESC[l;cR', where l is the line number (1-based), - // c is the column number (also 1-based), and R is the literal character - // 'R' (decimal code 82). + return `${ESC}[?1049h` +} - return `${ESC}[6n` - }, +export function disableAlternateScreen() { + return `${ESC}[?1049l` +} - enableAlternateScreen() { - // Enables alternate screen: - // "Xterm maintains two screen buffers. The normal screen buffer allows - // you to scroll back to view saved lines of output up to the maximum set - // by the saveLines resource. The alternate screen buffer is exactly as - // large as the display, contains no additional saved lines." +export function measureColumns(text) { + // Returns the number of columns the given text takes. Accounts for escape + // codes (by not including them in the returned width). - return `${ESC}[?1049h` - }, + if (text.includes(ESC)) { + text = text.replace(new RegExp(String.raw`${ESC}\[\??[0-9;]*.`, 'g'), '') + } - disableAlternateScreen() { - return `${ESC}[?1049l` - }, + return wcwidth(text) +} - measureColumns(text) { - // Returns the number of columns the given text takes. Accounts for escape - // codes (by not including them in the returned width). +export function trimToColumns(text, cols) { + // Trims off the end of the passed text so that its width doesn't exceed + // the size passed in columns. - if (text.includes(ESC)) { - text = text.replace(new RegExp(ESC + '\\[\\??[0-9;]*.', 'g'), '') + let out = '' + for (const char of text) { + if (measureColumns(out + char) <= cols) { + out += char + } else { + break } + } + return out +} - return wcwidth(text) - }, +export function wrapToColumns(text, cols) { + // Wraps a string into separate lines. Returns an array of strings, for + // each line of the text. - trimToColumns(text, cols) { - // Trims off the end of the passed text so that its width doesn't exceed - // the size passed in columns. + const lines = [] + const words = text.split(' ') - let out = '' - for (const char of text) { - if (ansi.measureColumns(out + char) <= cols) { - out += char - } else { - break - } - } - return out - }, - - isANSICommand(buffer, code = null) { - return ( - buffer[0] === 0x1b && buffer[1] === 0x5b && - (code ? buffer[buffer.length - 1] === code : true) - ) - }, - - interpret(text, scrRows, scrCols, { - oldChars = null, oldLastChar = null, - oldScrRows = null, oldScrCols = null, - oldCursorRow = 1, oldCursorCol = 1, oldShowCursor = true - } = {}) { - // Interprets the given ansi code, more or less. - - const blank = { - attributes: [], - char: ' ' + let curLine = words[0] + let curColumns = measureColumns(curLine) + + for (const word of words.slice(1)) { + const wordColumns = measureColumns(word) + if (curColumns + wordColumns > cols) { + lines.push(curLine) + curLine = word + curColumns = wordColumns + } else { + curLine += ' ' + word + curColumns += 1 + wordColumns } + } - const chars = new Array(scrRows * scrCols).fill(blank) + lines.push(curLine) - if (oldChars) { - for (let row = 0; row < scrRows && row < oldScrRows; row++) { - for (let col = 0; col < scrCols && col < oldScrCols; col++) { - chars[row * scrCols + col] = oldChars[row * oldScrCols + col] - } + return lines +} + +export function isANSICommand(buffer, code = null) { + return ( + buffer[0] === 0x1b && buffer[1] === 0x5b && + (code ? buffer[buffer.length - 1] === code : true) + ) +} + +export function interpret(text, scrRows, scrCols, { + oldChars = null, oldLastChar = null, + oldScrRows = null, oldScrCols = null, + oldCursorRow = 1, oldCursorCol = 1, oldShowCursor = true +} = {}) { + // Interprets the given ansi code, more or less. + + const blank = { + attributes: [], + char: ' ' + } + + const chars = new Array(scrRows * scrCols).fill(blank) + + if (oldChars) { + for (let row = 0; row < scrRows && row < oldScrRows; row++) { + for (let col = 0; col < scrCols && col < oldScrCols; col++) { + chars[row * scrCols + col] = oldChars[row * oldScrCols + col] } } + } - let showCursor = oldShowCursor - let cursorRow = oldCursorRow - let cursorCol = oldCursorCol - let attributes = [] + let showCursor = oldShowCursor + let cursorRow = oldCursorRow + let cursorCol = oldCursorCol + let attributes = [] - for (let charI = 0; charI < text.length; charI++) { - const cursorIndex = (cursorRow - 1) * scrCols + (cursorCol - 1) + for (let charI = 0; charI < text.length; charI++) { + const cursorIndex = (cursorRow - 1) * scrCols + (cursorCol - 1) - if (text[charI] === ESC) { - charI++ + if (text[charI] === ESC) { + charI++ - if (text[charI] !== '[') { - throw new Error('ESC not followed by [') - } + if (text[charI] !== '[') { + throw new Error('ESC not followed by [') + } + + charI++ + // Selective control sequences (look them up) - we can just skip the + // question mark. + if (text[charI] === '?') { + charI++ + } + + const args = [] + let val = '' + while (isDigit(text[charI])) { + val += text[charI] charI++ - // Selective control sequences (look them up) - we can just skip the - // question mark. - if (text[charI] === '?') { + if (text[charI] === ';') { charI++ + args.push(val) + val = '' + continue } + } + args.push(val) - const args = [] - let val = '' - while (isDigit(text[charI])) { - val += text[charI] - charI++ + // CUP - Cursor Position (moveCursor) + if (text[charI] === 'H') { + cursorRow = parseInt(args[0]) + cursorCol = parseInt(args[1]) + } - if (text[charI] === ';') { - charI++ - args.push(val) - val = '' - continue - } + // SM - Set Mode + if (text[charI] === 'h') { + if (args[0] === '25') { + showCursor = true } - args.push(val) + } - // CUP - Cursor Position (moveCursor) - if (text[charI] === 'H') { - cursorRow = parseInt(args[0]) - cursorCol = parseInt(args[1]) + // ED - Erase Display (clearScreen) + if (text[charI] === 'J') { + // ESC[2J - erase whole display + if (args[0] === '2') { + chars.fill(blank) + charI += 3 + cursorCol = 1 + cursorRow = 1 } - // SM - Set Mode - if (text[charI] === 'h') { - if (args[0] === '25') { - showCursor = true + // ESC[1J - erase to beginning + else if (args[0] === '1') { + for (let i = 0; i < cursorIndex; i++) { + chars[i * 2] = ' ' + chars[i * 2 + 1] = [] } } - // ED - Erase Display (clearScreen) - if (text[charI] === 'J') { - // ESC[2J - erase whole display - if (args[0] === '2') { - chars.fill(blank) - charI += 3 - cursorCol = 1 - cursorRow = 1 - } - - // ESC[1J - erase to beginning - else if (args[0] === '1') { - for (let i = 0; i < cursorIndex; i++) { - chars[i * 2] = ' ' - chars[i * 2 + 1] = [] - } + // ESC[0J - erase to end + else if (args.length === 0 || args[0] === '0') { + for (let i = cursorIndex; i < chars.length; i++) { + chars[i * 2] = ' ' + chars[i * 2 + 1] = [] } + } + } - // ESC[0J - erase to end - else if (args.length === 0 || args[0] === '0') { - for (let i = cursorIndex; i < chars.length; i++) { - chars[i * 2] = ' ' - chars[i * 2 + 1] = [] - } - } + // RM - Reset Mode + if (text[charI] === 'l') { + if (args[0] === '25') { + showCursor = false } + } - // RM - Reset Mode - if (text[charI] === 'l') { - if (args[0] === '25') { - showCursor = false + // SGR - Select Graphic Rendition + if (text[charI] === 'm') { + const removeAttribute = attr => { + if (attributes.includes(attr)) { + attributes = attributes.slice() + attributes.splice(attributes.indexOf(attr), 1) } } - // SGR - Select Graphic Rendition - if (text[charI] === 'm') { - const removeAttribute = attr => { - if (attributes.includes(attr)) { - attributes = attributes.slice() - attributes.splice(attributes.indexOf(attr), 1) + for (const arg of args) { + if (arg === '0') { + attributes = [] + } else if (arg === '22') { // Neither bold nor faint + removeAttribute('1') + removeAttribute('2') + } else if (arg === '23') { // Neither italic nor Fraktur + removeAttribute('3') + removeAttribute('20') + } else if (arg === '24') { // Not underlined + removeAttribute('4') + } else if (arg === '25') { // Blink off + removeAttribute('5') + } else if (arg === '27') { // Inverse off + removeAttribute('7') + } else if (arg === '28') { // Conceal off + removeAttribute('8') + } else if (arg === '29') { // Not crossed out + removeAttribute('9') + } else if (arg === '39') { // Default foreground + for (let i = 0; i < 10; i++) { + removeAttribute('3' + i) } - } - - for (const arg of args) { - if (arg === '0') { - attributes = [] - } else if (arg === '22') { // Neither bold nor faint - removeAttribute('1') - removeAttribute('2') - } else if (arg === '23') { // Neither italic nor Fraktur - removeAttribute('3') - removeAttribute('20') - } else if (arg === '24') { // Not underlined - removeAttribute('4') - } else if (arg === '25') { // Blink off - removeAttribute('5') - } else if (arg === '27') { // Inverse off - removeAttribute('7') - } else if (arg === '28') { // Conceal off - removeAttribute('8') - } else if (arg === '29') { // Not crossed out - removeAttribute('9') - } else if (arg === '39') { // Default foreground - for (let i = 0; i < 10; i++) { - removeAttribute('3' + i) - } - } else if (arg === '49') { // Default background - for (let i = 0; i < 10; i++) { - removeAttribute('4' + i) - } - } else { - attributes = attributes.concat([arg]) + } else if (arg === '49') { // Default background + for (let i = 0; i < 10; i++) { + removeAttribute('4' + i) } + } else { + attributes = attributes.concat([arg]) } } - - continue } - chars[cursorIndex] = { - char: text[charI], attributes - } + continue + } - // Some characters take up multiple columns, e.g. Japanese text. Take - // this into consideration when drawing. - const charColumns = wcwidth(text[charI]) - cursorCol += charColumns + chars[cursorIndex] = { + char: text[charI], attributes + } - // If the character takes up 2+ columns, treat columns past the first - // one (where the character is) as empty. (Note this is different from - // "blank", which represents an empty space character ' '.) - for (let i = 1; i < charColumns; i++) { - chars[cursorIndex + i] = {char: '', attributes: []} - } + // Some characters take up multiple columns, e.g. Japanese text. Take + // this into consideration when drawing. + const charColumns = wcwidth(text[charI]) + cursorCol += charColumns - if (cursorCol > scrCols) { - cursorCol = 1 - cursorRow++ - } + // If the character takes up 2+ columns, treat columns past the first + // one (where the character is) as empty. (Note this is different from + // "blank", which represents an empty space character ' '.) + for (let i = 1; i < charColumns; i++) { + chars[cursorIndex + i] = {char: '', attributes: []} } - // SPOooooOOoky diffing! ------------- - // - // - Search for series of differences. This means a collection of characters - // which have different text or attribute properties. - // - // - Figure out how to print these differences. Move the cursor to the beginning - // character's row/column, then print the differences. + if (cursorCol > scrCols) { + cursorCol = 1 + cursorRow++ + } + } - const newChars = chars + // SPOooooOOoky diffing! ------------- + // + // - Search for series of differences. This means a collection of characters + // which have different text or attribute properties. + // + // - Figure out how to print these differences. Move the cursor to the beginning + // character's row/column, then print the differences. + + const newChars = chars + + const differences = [] + + if (oldChars === null) { + differences.push(0) + differences.push(newChars.slice()) + } else { + const charsEqual = (oldChar, newChar) => { + if (oldChar.char !== newChar.char) { + return false + } - const differences = [] + let oldAttrs = oldChar.attributes.slice() + let newAttrs = newChar.attributes.slice() - if (oldChars === null) { - differences.push(0) - differences.push(newChars.slice()) - } else { - const charsEqual = (oldChar, newChar) => { - if (oldChar.char !== newChar.char) { + while (newAttrs.length) { + const attr = newAttrs.shift() + if (oldAttrs.includes(attr)) { + oldAttrs.splice(oldAttrs.indexOf(attr), 1) + } else { return false } + } - let oldAttrs = oldChar.attributes.slice() - let newAttrs = newChar.attributes.slice() - - while (newAttrs.length) { - const attr = newAttrs.shift() - if (oldAttrs.includes(attr)) { - oldAttrs.splice(oldAttrs.indexOf(attr), 1) - } else { - return false - } - } - - oldAttrs = oldChar.attributes.slice() - newAttrs = newChar.attributes.slice() + oldAttrs = oldChar.attributes.slice() + newAttrs = newChar.attributes.slice() - while (oldAttrs.length) { - const attr = oldAttrs.shift() - if (newAttrs.includes(attr)) { - newAttrs.splice(newAttrs.indexOf(attr), 1) - } else { - return false - } + while (oldAttrs.length) { + const attr = oldAttrs.shift() + if (newAttrs.includes(attr)) { + newAttrs.splice(newAttrs.indexOf(attr), 1) + } else { + return false } - - return true } - let curChars = null + return true + } - for (let i = 0; i < chars.length; i++) { - const oldChar = oldChars[i] - const newChar = newChars[i] + let curChars = null - // TODO: Some sort of "distance" before we should clear curDiff? - // It may take *less* characters if this diff and the next are merged - // (entering a single character is smaller than the length of the code - // used to move past that character). Probably not very significant of - // an impact, though. - if (charsEqual(oldChar, newChar)) { - curChars = null - } else { - if (curChars === null) { - curChars = [] - differences.push(i, curChars) - } + for (let i = 0; i < chars.length; i++) { + const oldChar = oldChars[i] + const newChar = newChars[i] - curChars.push(newChar) + // TODO: Some sort of "distance" before we should clear curDiff? + // It may take *less* characters if this diff and the next are merged + // (entering a single character is smaller than the length of the code + // used to move past that character). Probably not very significant of + // an impact, though. + if (charsEqual(oldChar, newChar)) { + curChars = null + } else { + if (curChars === null) { + curChars = [] + differences.push(i, curChars) } + + curChars.push(newChar) } } + } - // Character concatenation ----------- + // Character concatenation ----------- - let lastChar = oldLastChar || { - char: '', - attributes: [] - } + let lastChar = oldLastChar || { + char: '', + attributes: [] + } - const result = [] - - for (let parse = 0; parse < differences.length; parse += 2) { - const i = differences[parse] - const chars = differences[parse + 1] - - const col = i % scrCols - const row = (i - col) / scrCols - result.push(ansi.moveCursor(row, col)) - - for (const char of chars) { - const newAttributes = ( - char.attributes.filter(attr => !(lastChar.attributes.includes(attr))) - ) - - const removedAttributes = ( - lastChar.attributes.filter(attr => !(char.attributes.includes(attr))) - ) - - // The only way to practically remove any character attribute is to - // reset all of its attributes and then re-add its existing attributes. - // If we do that, there's no need to add new attributes. - if (removedAttributes.length) { - result.push(ansi.resetAttributes()) - result.push(`${ESC}[${char.attributes.join(';')}m`) - } else if (newAttributes.length) { - result.push(`${ESC}[${newAttributes.join(';')}m`) - } + const result = [] + + for (let parse = 0; parse < differences.length; parse += 2) { + const i = differences[parse] + const chars = differences[parse + 1] + + const col = i % scrCols + const row = (i - col) / scrCols + result.push(moveCursor(row, col)) + + for (const char of chars) { + const newAttributes = ( + char.attributes.filter(attr => !(lastChar.attributes.includes(attr))) + ) + + const removedAttributes = ( + lastChar.attributes.filter(attr => !(char.attributes.includes(attr))) + ) + + // The only way to practically remove any character attribute is to + // reset all of its attributes and then re-add its existing attributes. + // If we do that, there's no need to add new attributes. + if (removedAttributes.length) { + result.push(resetAttributes()) + result.push(`${ESC}[${char.attributes.join(';')}m`) + } else if (newAttributes.length) { + result.push(`${ESC}[${newAttributes.join(';')}m`) + } - result.push(char.char) + result.push(char.char) - lastChar = char - } + lastChar = char } + } - // If anything changed *or* the cursor moved, we need to put it back where - // it was before: - if (result.length || cursorCol !== oldCursorCol || cursorRow !== oldCursorRow) { - result.push(ansi.moveCursor(cursorRow, cursorCol)) - } + // If anything changed *or* the cursor moved, we need to put it back where + // it was before: + if (result.length || cursorCol !== oldCursorCol || cursorRow !== oldCursorRow) { + result.push(moveCursor(cursorRow, cursorCol)) + } - // If the cursor is visible and wasn't before, or vice versa, we need to - // show that: - if (showCursor && !oldShowCursor) { - result.push(ansi.showCursor()) - } else if (!showCursor && oldShowCursor) { - result.push(ansi.hideCursor()) - } + // If the cursor is visible and wasn't before, or vice versa, we need to + // show that: + if (showCursor && !oldShowCursor) { + result.push(showCursor()) + } else if (!showCursor && oldShowCursor) { + result.push(hideCursor()) + } - return { - oldChars: newChars.slice(), - oldLastChar: Object.assign({}, lastChar), - oldScrRows: scrRows, - oldScrCols: scrCols, - oldCursorRow: cursorRow, - oldCursorCol: cursorCol, - oldShowCursor: showCursor, - screen: result.join('') - } + return { + oldChars: newChars.slice(), + oldLastChar: Object.assign({}, lastChar), + oldScrRows: scrRows, + oldScrCols: scrCols, + oldCursorRow: cursorRow, + oldCursorCol: cursorCol, + oldShowCursor: showCursor, + screen: result.join('') } } - -module.exports = ansi diff --git a/util/count.js b/util/count.js index 24c11b0..d4c0919 100644 --- a/util/count.js +++ b/util/count.js @@ -1,4 +1,4 @@ -module.exports = function count(arr) { +export default function count(arr) { // Counts the number of times the items of an array appear (only on the top // level; it doesn't search through nested arrays!). Returns a map of // item -> count. diff --git a/util/exception.js b/util/exception.js index e88ff99..1271b6a 100644 --- a/util/exception.js +++ b/util/exception.js @@ -1,4 +1,4 @@ -module.exports = function exception(code, message) { +export default function exception(code, message) { // Makes a custom error with the given code and message. const err = new Error(`${code}: ${message}`) diff --git a/util/index.js b/util/index.js new file mode 100644 index 0000000..43a08bd --- /dev/null +++ b/util/index.js @@ -0,0 +1,10 @@ +export * as ansi from './ansi.js' +export * as interfaces from './interfaces/index.js' + +export {default as count} from './count.js' +export {default as exception} from './exception.js' +export {default as smoothen} from './smoothen.js' +export {default as telchars} from './telchars.js' +export {default as tuiApp} from './tui-app.js' +export {default as unichars} from './unichars.js' +export {default as waitForData} from './waitForData.js' diff --git a/util/CommandLineInterfacer.js b/util/interfaces/CommandLineInterface.js index d2007fb..66c8c43 100644 --- a/util/CommandLineInterfacer.js +++ b/util/interfaces/CommandLineInterface.js @@ -1,8 +1,9 @@ -const EventEmitter = require('events') -const waitForData = require('./waitForData') -const ansi = require('./ansi') +import EventEmitter from 'node:events' -module.exports = class CommandLineInterfacer extends EventEmitter { +import * as ansi from '../ansi.js' +import waitForData from '../waitForData.js' + +export default class CommandLineInterface extends EventEmitter { constructor(inStream = process.stdin, outStream = process.stdout, proc = process) { super() diff --git a/util/Flushable.js b/util/interfaces/Flushable.js index 058d186..d8b72d3 100644 --- a/util/Flushable.js +++ b/util/interfaces/Flushable.js @@ -1,7 +1,7 @@ -const ansi = require('./ansi') -const unic = require('./unichars') +import * as ansi from '../ansi.js' +import unic from '../unichars.js' -module.exports = class Flushable { +export default class Flushable { // A writable that can be used to collect chunks of data before writing // them. diff --git a/util/TelnetInterfacer.js b/util/interfaces/TelnetInterface.js index dc71157..8777680 100644 --- a/util/TelnetInterfacer.js +++ b/util/interfaces/TelnetInterface.js @@ -1,8 +1,9 @@ -const ansi = require('./ansi') -const waitForData = require('./waitForData') -const EventEmitter = require('events') +import EventEmitter from 'node:events' -module.exports = class TelnetInterfacer extends EventEmitter { +import * as ansi from '../ansi.js' +import waitForData from '../waitForData.js' + +export default class TelnetInterface extends EventEmitter { constructor(socket) { super() diff --git a/util/interfaces/index.js b/util/interfaces/index.js new file mode 100644 index 0000000..83aeb2c --- /dev/null +++ b/util/interfaces/index.js @@ -0,0 +1,4 @@ +export {default as Flushable} from './Flushable.js' + +export {default as CommandLineInterface} from './CommandLineInterface.js' +export {default as TelnetInterface} from './TelnetInterface.js' diff --git a/util/smoothen.js b/util/smoothen.js index 55ba23c..5809271 100644 --- a/util/smoothen.js +++ b/util/smoothen.js @@ -1,4 +1,4 @@ -module.exports = function(tx, x, divisor) { +export default function smoothen(tx, x, divisor) { // Smoothly transitions givens X to TX using a given divisor. Rounds the // amount moved. diff --git a/util/telchars.js b/util/telchars.js index 12d4095..5a5ad42 100644 --- a/util/telchars.js +++ b/util/telchars.js @@ -97,4 +97,4 @@ const telchars = { isCharacter: (buf, char) => compareBufStr(buf, char), } -module.exports = telchars +export default telchars diff --git a/util/tui-app.js b/util/tui-app.js index fe1cd03..0dfd821 100644 --- a/util/tui-app.js +++ b/util/tui-app.js @@ -2,27 +2,26 @@ // program. Contained to reduce boilerplate and improve consistency between // programs. -const ansi = require('./ansi'); +import {Root} from 'tui-lib/ui/primitives' -const CommandLineInterfacer = require('./CommandLineInterfacer'); -const Flushable = require('./Flushable'); -const Root = require('../ui/Root'); +import {CommandLineInterface, Flushable} from './interfaces/index.js' +import * as ansi from './ansi.js' -module.exports = async function tuiApp(callback) { - // TODO: Support other interfacers. - const interfacer = new CommandLineInterfacer(); +export default async function tuiApp(callback) { + // TODO: Support other screen interfaces. + const screenInterface = new CommandLineInterface(); const flushable = new Flushable(process.stdout, true); - const root = new Root(interfacer, flushable); + const root = new Root(screenInterface, flushable); - const size = await interfacer.getScreenSize(); + const size = await screenInterface.getScreenSize(); root.w = size.width; root.h = size.height; flushable.resizeScreen(size); root.on('rendered', () => flushable.flush()); - interfacer.on('resize', newSize => { + screenInterface.on('resize', newSize => { root.w = newSize.width; root.h = newSize.height; flushable.resizeScreen(newSize); diff --git a/util/unichars.js b/util/unichars.js index ee137e8..2099b62 100644 --- a/util/unichars.js +++ b/util/unichars.js @@ -1,6 +1,6 @@ // Useful Unicode characters. -module.exports = { +export default { /* … */ ELLIPSIS: '\u2026', /* ─ */ BOX_H: '\u2500', diff --git a/util/waitForData.js b/util/waitForData.js index ed88402..75f740e 100644 --- a/util/waitForData.js +++ b/util/waitForData.js @@ -1,4 +1,4 @@ -module.exports = function waitForData(stream, cond = null) { +export default function waitForData(stream, cond = null) { return new Promise(resolve => { const listener = data => { if (cond ? cond(data) : true) { diff --git a/util/wrap.js b/util/wrap.js deleted file mode 100644 index 71a1f1c..0000000 --- a/util/wrap.js +++ /dev/null @@ -1,28 +0,0 @@ -const ansi = require('./ansi') - -module.exports = function wrap(str, width) { - // Wraps a string into separate lines. Returns an array of strings, for - // each line of the text. - - const lines = [] - const words = str.split(' ') - - let curLine = words[0] - let curColumns = ansi.measureColumns(curLine) - - for (const word of words.slice(1)) { - const wordColumns = ansi.measureColumns(word) - if (curColumns + wordColumns > width) { - lines.push(curLine) - curLine = word - curColumns = wordColumns - } else { - curLine += ' ' + word - curColumns += 1 + wordColumns - } - } - - lines.push(curLine) - - return lines -} |