[gjs/ewlsh/nova-repl] Use Node.js readline
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/nova-repl] Use Node.js readline
- Date: Mon, 24 Jan 2022 05:25:48 +0000 (UTC)
commit 1ad24d23c4afec06cc32fb3f436f697ceb870aa4
Author: Evan Welsh <contact evanwelsh com>
Date: Sun Jan 23 21:25:42 2022 -0800
Use Node.js readline
.eslintignore | 1 -
.gitlab-ci.yml | 2 +-
installed-tests/js/meson.build | 1 +
installed-tests/js/testRepl.js | 135 ++++++
js.gresource.xml | 5 +-
modules/esm/_bootstrap/repl.js | 2 +-
modules/esm/_repl/callbacks.js | 101 +++++
modules/esm/_repl/cliffy.js | 929 ---------------------------------------
modules/esm/_repl/primordials.js | 33 ++
modules/esm/_repl/utils.js | 427 ++++++++++++++++++
modules/esm/events.js | 104 +++++
modules/esm/repl.js | 729 +++++++++++++++---------------
modules/script/console.js | 2 +-
tools/cliffy/.eslintrc.yml | 7 -
tools/cliffy/ansi.js | 8 -
tools/cliffy/bundle.sh | 7 -
tools/cliffy/lib.js | 30 --
tools/cliffy/transform.js | 22 -
18 files changed, 1164 insertions(+), 1381 deletions(-)
---
diff --git a/.eslintignore b/.eslintignore
index e61a4aaba..372ae4593 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -5,6 +5,5 @@ installed-tests/js/jasmine.js
installed-tests/js/modules/badOverrides/WarnLib.js
installed-tests/js/modules/subBadInit/__init__.js
modules/script/jsUnit.js
-modules/esm/_repl/cliffy.js
/_build
/builddir
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cf4cad15a..5c4a9e33f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -266,7 +266,7 @@ codespell:
stage: source_check
script:
- codespell --version
- - codespell -S "*.png,*.po,*.jpg,*.wrap,.git,LICENSES" -f --builtin "code,usage,clear"
--skip="./installed-tests/js/jasmine.js,./README.md,./build/flatpak/*.json,./modules/esm/_repl/cliffy.js"
--ignore-words-list="afterall,befores,files',filetest,gir,inout,stdio,uint,upto,xdescribe"
+ - codespell -S "*.png,*.po,*.jpg,*.wrap,.git,LICENSES" -f --builtin "code,usage,clear"
--skip="./installed-tests/js/jasmine.js,./README.md,./build/flatpak/*.json,./modules/esm/_repl/*.js"
--ignore-words-list="afterall,befores,files',filetest,gir,inout,stdio,uint,upto,xdescribe"
except:
- schedules
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index 6a5c40b50..17796eeb5 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -233,6 +233,7 @@ endif
modules_tests = [
'Async',
'Console',
+ 'Repl',
'ESModules',
'Encoding',
'GLibLogWriter',
diff --git a/installed-tests/js/testRepl.js b/installed-tests/js/testRepl.js
new file mode 100644
index 000000000..4b3b80f85
--- /dev/null
+++ b/installed-tests/js/testRepl.js
@@ -0,0 +1,135 @@
+import Gio from 'gi://Gio';
+
+import { AsyncReadline } from 'repl';
+
+function createReadline() {
+ return new AsyncReadline({ stdin: null, stdout: null, stderr: null, prompt: '> ', enableColor: false });
+}
+
+function createReadlineWithStreams() {
+ const stdin = new Gio.MemoryInputStream();
+ const stdout = Gio.MemoryOutputStream.new_resizable();
+ const stderr = Gio.MemoryOutputStream.new_resizable();
+
+ const readline = new AsyncReadline({ stdin, stdout, stderr, prompt: '> ', enableColor: false });
+
+ return {
+ readline, async teardown() {
+ readline.cancel();
+
+ try {
+ readline.stdout.close(null);
+ } catch { }
+
+ try {
+ readline.stdin.close(null);
+ } catch { }
+
+ try {
+ readline.stderr.close(null);
+ } catch { }
+ }
+ };
+}
+
+function expectReadlineOutput({ readline, input, output, keystrokes = 1 }) {
+ return new Promise((resolve) => {
+ let renderCount = 0;
+
+ readline.connect('render', () => {
+ if (++renderCount === keystrokes) {
+ readline.disconnectAll();
+
+ expect(readline.line).toBe(output);
+ resolve();
+ }
+ });
+
+ readline.stdin.add_bytes(new TextEncoder().encode(input));
+ });
+}
+
+describe('Repl', () => {
+ it('handles key events on stdin', async function () {
+ const { readline, teardown } = createReadlineWithStreams();
+
+ readline.prompt();
+
+ await expectReadlineOutput({
+ readline,
+ input: 'a',
+ output: 'a'
+ });
+
+ await expectReadlineOutput({
+ readline,
+ input: 'b',
+ output: 'ab'
+ });
+
+ await expectReadlineOutput({
+ readline,
+ input: '\x1b[D\x1b[Dcr',
+ output: 'crab',
+ keystrokes: 4
+ });
+
+ teardown();
+ });
+});
+
+describe('Readline', () => {
+
+ it('can move word left', function () {
+ const readline = createReadline();
+
+ readline.line = 'lorem ipsum';
+ readline.cursor = readline.line.length;
+
+ readline.wordLeft();
+
+ expect(readline.line).toBe('lorem ipsum');
+ expect(readline.cursor).toBe('lorem '.length);
+ });
+
+
+ it('can move word right', function () {
+ const readline = createReadline();
+
+ readline.line = 'lorem ipsum';
+ readline.cursor = 0;
+
+ readline.wordRight();
+
+ expect(readline.line).toBe('lorem ipsum');
+ expect(readline.cursor).toBe('lorem '.length);
+ });
+
+ it('can delete word left', function () {
+ const readline = createReadline();
+
+ readline.line = 'lorem ipsum';
+ readline.cursor = readline.line.length;
+
+ readline.deleteWordLeft();
+
+ const output = 'lorem ';
+
+ expect(readline.line).toBe(output);
+ expect(readline.cursor).toBe(output.length);
+ });
+
+ it('can delete word right', function () {
+ const readline = createReadline();
+
+ readline.line = 'lorem ipsum';
+ readline.cursor = 0;
+
+ readline.deleteWordRight();
+
+ const output = 'ipsum';
+
+ expect(readline.line).toBe(output);
+ expect(readline.cursor).toBe(0);
+ });
+});
\ No newline at end of file
diff --git a/js.gresource.xml b/js.gresource.xml
index 6817f0b32..ba3212ed4 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -17,12 +17,15 @@
<file>modules/esm/_timers.js</file>
- <file>modules/esm/_repl/cliffy.js</file>
+ <file>modules/esm/_repl/utils.js</file>
+ <file>modules/esm/_repl/primordials.js</file>
+ <file>modules/esm/_repl/callbacks.js</file>
<file>modules/esm/cairo.js</file>
<file>modules/esm/gettext.js</file>
<file>modules/esm/console.js</file>
<file>modules/esm/gi.js</file>
+ <file>modules/esm/events.js</file>
<file>modules/esm/repl.js</file>
<file>modules/esm/system.js</file>
diff --git a/modules/esm/_bootstrap/repl.js b/modules/esm/_bootstrap/repl.js
index fa51a1fbe..cb8e98eb0 100644
--- a/modules/esm/_bootstrap/repl.js
+++ b/modules/esm/_bootstrap/repl.js
@@ -7,4 +7,4 @@ const repl = new Repl();
globalThis.repl = repl;
-repl.run();
+repl.start();
diff --git a/modules/esm/_repl/callbacks.js b/modules/esm/_repl/callbacks.js
new file mode 100644
index 000000000..375f86ae1
--- /dev/null
+++ b/modules/esm/_repl/callbacks.js
@@ -0,0 +1,101 @@
+/* eslint-disable no-nested-ternary */
+'use strict';
+
+/* eslint-disable max-statements-per-line */
+/* eslint-disable jsdoc/require-param-description */
+/* eslint-disable jsdoc/require-param-type */
+
+import {primordials} from './primordials.js';
+
+const {
+ NumberIsNaN,
+} = primordials;
+
+const ERR_INVALID_ARG_VALUE = Error;
+const ERR_INVALID_CURSOR_POS = Error;
+
+// Adapted from
https://github.com/nodejs/node/blob/56679eb53044b03e4da0f7420774d54f0c550eec/lib/internal/readline/callbacks.js
+
+import {CSI} from './utils.js';
+
+const {
+ kClearLine,
+ kClearToLineBeginning,
+ kClearToLineEnd,
+} = CSI;
+
+
+/**
+ * moves the cursor to the x and y coordinate on the given stream
+ */
+
+/**
+ * @param x
+ * @param y
+ */
+function cursorTo(x, y) {
+ if (NumberIsNaN(x))
+ throw new ERR_INVALID_ARG_VALUE('x', x);
+ if (NumberIsNaN(y))
+ throw new ERR_INVALID_ARG_VALUE('y', y);
+
+ if (typeof x !== 'number')
+ throw new ERR_INVALID_CURSOR_POS();
+
+ const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
+ return data;
+}
+
+/**
+ * moves the cursor relative to its current location
+ */
+
+/**
+ * @param dx
+ * @param dy
+ */
+function moveCursor(dx, dy) {
+ let data = '';
+
+ if (dx < 0)
+ data += CSI`${-dx}D`;
+ else if (dx > 0)
+ data += CSI`${dx}C`;
+
+
+ if (dy < 0)
+ data += CSI`${-dy}A`;
+ else if (dy > 0)
+ data += CSI`${dy}B`;
+
+
+ return data;
+}
+
+/**
+ * clears the current line the cursor is on:
+ * -1 for left of the cursor
+ * +1 for right of the cursor
+ * 0 for the entire line
+ */
+
+/**
+ * @param dir
+ */
+function clearLine(dir) {
+ const type =
+ dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
+ return type;
+}
+
+
+
+/**
+ * clears the screen from the current position of the cursor down
+ */
+
+export {
+ clearLine,
+ cursorTo,
+ moveCursor
+};
diff --git a/modules/esm/_repl/primordials.js b/modules/esm/_repl/primordials.js
new file mode 100644
index 000000000..d2825fbea
--- /dev/null
+++ b/modules/esm/_repl/primordials.js
@@ -0,0 +1,33 @@
+/**
+ * @typedef {F extends ((...args: infer Args) => infer Result) ? ((instance: I, ...args: Args) => Result) :
never} UncurriedFunction
+ * @template I
+ * @template F
+ */
+
+/**
+ * @template {Record<string, any>} T
+ * @template {keyof T} K
+ * @param {T} [type] the instance type for the function
+ * @param {K} key the function to curry
+ * @returns {UncurriedFunction<T, T[K]>}
+ */
+function uncurryThis(type, key) {
+ const func = type[key];
+ return (instance, ...args) => func.apply(instance, args);
+}
+
+const primordials = {
+ ArrayPrototypeSlice: uncurryThis(Array.prototype, 'slice'),
+ ArrayPrototypeSort: uncurryThis(Array.prototype, 'sort'),
+ RegExpPrototypeTest: uncurryThis(RegExp.prototype, 'test'),
+ StringFromCharCode: String.fromCharCode,
+ StringPrototypeCharCodeAt: uncurryThis(String.prototype, 'charCodeAt'),
+ StringPrototypeCodePointAt: uncurryThis(String.prototype, 'codePointAt'),
+ StringPrototypeMatch: uncurryThis(String.prototype, 'match'),
+ StringPrototypeSlice: uncurryThis(String.prototype, 'slice'),
+ StringPrototypeToLowerCase: uncurryThis(String.prototype, 'toLowerCase'),
+ Symbol,
+ NumberIsNaN: Number.isNaN,
+};
+
+export {primordials, uncurryThis};
diff --git a/modules/esm/_repl/utils.js b/modules/esm/_repl/utils.js
new file mode 100644
index 000000000..79995e30b
--- /dev/null
+++ b/modules/esm/_repl/utils.js
@@ -0,0 +1,427 @@
+/* eslint-disable max-statements-per-line */
+/* eslint-disable jsdoc/require-param-description */
+/* eslint-disable jsdoc/require-param-type */
+
+import {primordials} from './primordials.js';
+
+// From
https://github.com/nodejs/node/blob/56679eb53044b03e4da0f7420774d54f0c550eec/lib/internal/readline/utils.js
+
+const {
+ ArrayPrototypeSlice,
+ ArrayPrototypeSort,
+ RegExpPrototypeTest,
+ StringFromCharCode,
+ StringPrototypeCharCodeAt,
+ StringPrototypeCodePointAt,
+ StringPrototypeMatch,
+ StringPrototypeSlice,
+ StringPrototypeToLowerCase,
+ Symbol,
+} = primordials;
+
+/**
+ * @typedef {object} KeyDefinition
+ * @property {string} name
+ * @property {string} sequence
+ * @property {boolean} ctrl
+ * @property {boolean} meta
+ * @property {boolean} shift
+ */
+
+/**
+ * @typedef {object} KeyEvent
+ * @property {string | undefined} sequence
+ * @property {KeyDefinition} key
+ */
+
+const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
+const kEscape = '\x1b';
+const kSubstringSearch = Symbol('kSubstringSearch');
+
+/**
+ * @param strings
+ * @param {...any} args
+ */
+function CSI(strings, ...args) {
+ let ret = `${kEscape}[`;
+ for (let n = 0; n < strings.length; n++) {
+ ret += strings[n];
+ if (n < args.length)
+ ret += args[n];
+ }
+ return ret;
+}
+
+CSI.kEscape = kEscape;
+CSI.kClearToLineBeginning = CSI`1K`;
+CSI.kClearToLineEnd = CSI`0K`;
+CSI.kClearLine = CSI`2K`;
+CSI.kClearScreenDown = CSI`0J`;
+
+// TODO(BridgeAR): Treat combined characters as single character, i.e,
+// 'a\u0301' and '\u0301a' (both have the same visual output).
+// Check Canonical_Combining_Class in
+// http://userguide.icu-project.org/strings/properties
+/**
+ * @param str
+ * @param i
+ */
+function charLengthLeft(str, i) {
+ if (i <= 0)
+ return 0;
+ if ((i > 1 &&
+ StringPrototypeCodePointAt(str, i - 2) >= kUTF16SurrogateThreshold) ||
+ StringPrototypeCodePointAt(str, i - 1) >= kUTF16SurrogateThreshold)
+ return 2;
+
+ return 1;
+}
+
+/**
+ * @param str
+ * @param i
+ */
+function charLengthAt(str, i) {
+ if (str.length <= i) {
+ // Pretend to move to the right. This is necessary to autocomplete while
+ // moving to the right.
+ return 1;
+ }
+ return StringPrototypeCodePointAt(str, i) >= kUTF16SurrogateThreshold ? 2 : 1;
+}
+
+/*
+ Some patterns seen in terminal key escape codes, derived from combos seen
+ at http://www.midnight-commander.org/browser/lib/tty/key.c
+
+ ESC letter
+ ESC [ letter
+ ESC [ modifier letter
+ ESC [ 1 ; modifier letter
+ ESC [ num char
+ ESC [ num ; modifier char
+ ESC O letter
+ ESC O modifier letter
+ ESC O 1 ; modifier letter
+ ESC N letter
+ ESC [ [ num ; modifier char
+ ESC [ [ 1 ; modifier letter
+ ESC ESC [ num char
+ ESC ESC O letter
+
+ - char is usually ~ but $ and ^ also happen with rxvt
+ - modifier is 1 +
+ (shift * 1) +
+ (left_alt * 2) +
+ (ctrl * 4) +
+ (right_alt * 8)
+ - two leading ESCs apparently mean the same as one leading ESC
+*/
+
+/**
+ * @param callback
+ * @returns {Generator<KeyEvent | undefined, never, string>}
+ */
+function* emitKeys(callback) {
+ while (true) {
+ let ch = yield;
+ let s = ch;
+ let escaped = false;
+ const key = {
+ sequence: null,
+ name: undefined,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ };
+
+ if (ch === kEscape) {
+ escaped = true;
+ s += ch = yield;
+
+ if (ch === kEscape)
+ s += ch = yield;
+ }
+
+ if (escaped && (ch === 'O' || ch === '[')) {
+ // ANSI escape sequence
+ let code = ch;
+ let modifier = 0;
+
+ if (ch === 'O') {
+ // ESC O letter
+ // ESC O modifier letter
+ s += ch = yield;
+
+ if (ch >= '0' && ch <= '9') {
+ modifier = (ch >> 0) - 1;
+ s += ch = yield;
+ }
+
+ code += ch;
+ } else if (ch === '[') {
+ // ESC [ letter
+ // ESC [ modifier letter
+ // ESC [ [ modifier letter
+ // ESC [ [ num char
+ s += ch = yield;
+
+ if (ch === '[') {
+ // \x1b[[A
+ // ^--- escape codes might have a second bracket
+ code += ch;
+ s += ch = yield;
+ }
+
+ /*
+ * Here and later we try to buffer just enough data to get
+ * a complete ascii sequence.
+ *
+ * We have basically two classes of ascii characters to process:
+ *
+ *
+ * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
+ *
+ * This particular example is featuring Ctrl+F12 in xterm.
+ *
+ * - `;5` part is optional, e.g. it could be `\x1b[24~`
+ * - first part can contain one or two digits
+ *
+ * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
+ *
+ *
+ * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
+ *
+ * This particular example is featuring Ctrl+Home in xterm.
+ *
+ * - `1;5` part is optional, e.g. it could be `\x1b[H`
+ * - `1;` part is optional, e.g. it could be `\x1b[5H`
+ *
+ * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
+ *
+ */
+ const cmdStart = s.length - 1;
+
+ // Skip one or two leading digits
+ if (ch >= '0' && ch <= '9') {
+ s += ch = yield;
+
+ if (ch >= '0' && ch <= '9')
+ s += ch = yield;
+ }
+
+ // skip modifier
+ if (ch === ';') {
+ s += ch = yield;
+
+ if (ch >= '0' && ch <= '9')
+ s += yield;
+ }
+
+ /*
+ * We buffered enough data, now trying to extract code
+ * and modifier from it
+ */
+ const cmd = StringPrototypeSlice(s, cmdStart);
+ let match;
+
+ if ((match = StringPrototypeMatch(cmd, /^(\d\d?)(;(\d))?([~^$])$/))) {
+ code += match[1] + match[4];
+ modifier = (match[3] || 1) - 1;
+ } else if (
+ (match = StringPrototypeMatch(cmd, /^((\d;)?(\d))?([A-Za-z])$/))
+ ) {
+ code += match[4];
+ modifier = (match[3] || 1) - 1;
+ } else {
+ code += cmd;
+ }
+ }
+
+ // Parse the key modifier
+ key.ctrl = !!(modifier & 4);
+ key.meta = !!(modifier & 10);
+ key.shift = !!(modifier & 1);
+ key.code = code;
+
+ // Parse the key itself
+ switch (code) {
+ /* xterm/gnome ESC [ letter (with modifier) */
+ case '[P': key.name = 'f1'; break;
+ case '[Q': key.name = 'f2'; break;
+ case '[R': key.name = 'f3'; break;
+ case '[S': key.name = 'f4'; break;
+
+ /* xterm/gnome ESC O letter (without modifier) */
+ case 'OP': key.name = 'f1'; break;
+ case 'OQ': key.name = 'f2'; break;
+ case 'OR': key.name = 'f3'; break;
+ case 'OS': key.name = 'f4'; break;
+
+ /* xterm/rxvt ESC [ number ~ */
+ case '[11~': key.name = 'f1'; break;
+ case '[12~': key.name = 'f2'; break;
+ case '[13~': key.name = 'f3'; break;
+ case '[14~': key.name = 'f4'; break;
+
+ /* from Cygwin and used in libuv */
+ case '[[A': key.name = 'f1'; break;
+ case '[[B': key.name = 'f2'; break;
+ case '[[C': key.name = 'f3'; break;
+ case '[[D': key.name = 'f4'; break;
+ case '[[E': key.name = 'f5'; break;
+
+ /* common */
+ case '[15~': key.name = 'f5'; break;
+ case '[17~': key.name = 'f6'; break;
+ case '[18~': key.name = 'f7'; break;
+ case '[19~': key.name = 'f8'; break;
+ case '[20~': key.name = 'f9'; break;
+ case '[21~': key.name = 'f10'; break;
+ case '[23~': key.name = 'f11'; break;
+ case '[24~': key.name = 'f12'; break;
+
+ /* xterm ESC [ letter */
+ case '[A': key.name = 'up'; break;
+ case '[B': key.name = 'down'; break;
+ case '[C': key.name = 'right'; break;
+ case '[D': key.name = 'left'; break;
+ case '[E': key.name = 'clear'; break;
+ case '[F': key.name = 'end'; break;
+ case '[H': key.name = 'home'; break;
+
+ /* xterm/gnome ESC O letter */
+ case 'OA': key.name = 'up'; break;
+ case 'OB': key.name = 'down'; break;
+ case 'OC': key.name = 'right'; break;
+ case 'OD': key.name = 'left'; break;
+ case 'OE': key.name = 'clear'; break;
+ case 'OF': key.name = 'end'; break;
+ case 'OH': key.name = 'home'; break;
+
+ /* xterm/rxvt ESC [ number ~ */
+ case '[1~': key.name = 'home'; break;
+ case '[2~': key.name = 'insert'; break;
+ case '[3~': key.name = 'delete'; break;
+ case '[4~': key.name = 'end'; break;
+ case '[5~': key.name = 'pageup'; break;
+ case '[6~': key.name = 'pagedown'; break;
+
+ /* putty */
+ case '[[5~': key.name = 'pageup'; break;
+ case '[[6~': key.name = 'pagedown'; break;
+
+ /* rxvt */
+ case '[7~': key.name = 'home'; break;
+ case '[8~': key.name = 'end'; break;
+
+ /* rxvt keys with modifiers */
+ case '[a': key.name = 'up'; key.shift = true; break;
+ case '[b': key.name = 'down'; key.shift = true; break;
+ case '[c': key.name = 'right'; key.shift = true; break;
+ case '[d': key.name = 'left'; key.shift = true; break;
+ case '[e': key.name = 'clear'; key.shift = true; break;
+
+ case '[2$': key.name = 'insert'; key.shift = true; break;
+ case '[3$': key.name = 'delete'; key.shift = true; break;
+ case '[5$': key.name = 'pageup'; key.shift = true; break;
+ case '[6$': key.name = 'pagedown'; key.shift = true; break;
+ case '[7$': key.name = 'home'; key.shift = true; break;
+ case '[8$': key.name = 'end'; key.shift = true; break;
+
+ case 'Oa': key.name = 'up'; key.ctrl = true; break;
+ case 'Ob': key.name = 'down'; key.ctrl = true; break;
+ case 'Oc': key.name = 'right'; key.ctrl = true; break;
+ case 'Od': key.name = 'left'; key.ctrl = true; break;
+ case 'Oe': key.name = 'clear'; key.ctrl = true; break;
+
+ case '[2^': key.name = 'insert'; key.ctrl = true; break;
+ case '[3^': key.name = 'delete'; key.ctrl = true; break;
+ case '[5^': key.name = 'pageup'; key.ctrl = true; break;
+ case '[6^': key.name = 'pagedown'; key.ctrl = true; break;
+ case '[7^': key.name = 'home'; key.ctrl = true; break;
+ case '[8^': key.name = 'end'; key.ctrl = true; break;
+
+ /* misc. */
+ case '[Z': key.name = 'tab'; key.shift = true; break;
+ default: key.name = 'undefined'; break;
+ }
+ } else if (ch === '\r') {
+ // carriage return
+ key.name = 'return';
+ key.meta = escaped;
+ } else if (ch === '\n') {
+ // Enter, should have been called linefeed
+ key.name = 'enter';
+ key.meta = escaped;
+ } else if (ch === '\t') {
+ // tab
+ key.name = 'tab';
+ key.meta = escaped;
+ } else if (ch === '\b' || ch === '\x7f') {
+ // backspace or ctrl+h
+ key.name = 'backspace';
+ key.meta = escaped;
+ } else if (ch === kEscape) {
+ // escape key
+ key.name = 'escape';
+ key.meta = escaped;
+ } else if (ch === ' ') {
+ key.name = 'space';
+ key.meta = escaped;
+ } else if (!escaped && ch <= '\x1a') {
+ // ctrl+letter
+ key.name = StringFromCharCode(
+ StringPrototypeCharCodeAt(ch) + StringPrototypeCharCodeAt('a') - 1
+ );
+ key.ctrl = true;
+ } else if (RegExpPrototypeTest(/^[0-9A-Za-z]$/, ch)) {
+ // Letter, number, shift+letter
+ key.name = StringPrototypeToLowerCase(ch);
+ key.shift = RegExpPrototypeTest(/^[A-Z]$/, ch);
+ key.meta = escaped;
+ } else if (escaped) {
+ // Escape sequence timeout
+ key.name = ch.length ? undefined : 'escape';
+ key.meta = true;
+ }
+
+ key.sequence = s;
+
+ if (s.length !== 0 && (key.name !== undefined || escaped)) {
+ /* Named character or sequence */
+ callback(escaped ? undefined : s, key);
+ } else if (charLengthAt(s, 0) === s.length) {
+ /* Single unnamed character, e.g. "." */
+ callback(s, key);
+ }
+ /* Unrecognized or broken escape sequence, don't emit anything */
+ }
+}
+
+// This runs in O(n log n).
+/**
+ * @param strings
+ */
+function commonPrefix(strings) {
+ if (strings.length === 1)
+ return strings[0];
+
+ const sorted = ArrayPrototypeSort(ArrayPrototypeSlice(strings));
+ const min = sorted[0];
+ const max = sorted[sorted.length - 1];
+ for (let i = 0; i < min.length; i++) {
+ if (min[i] !== max[i])
+ return StringPrototypeSlice(min, 0, i);
+ }
+ return min;
+}
+
+export {
+ charLengthAt,
+ charLengthLeft,
+ commonPrefix,
+ emitKeys,
+ kSubstringSearch,
+ CSI
+};
diff --git a/modules/esm/events.js b/modules/esm/events.js
new file mode 100644
index 000000000..0eb646428
--- /dev/null
+++ b/modules/esm/events.js
@@ -0,0 +1,104 @@
+export class EventEmitter {
+ #signalConnections = [];
+ #nextConnectionId = 1;
+
+ connect(name, callback) {
+ // be paranoid about callback arg since we'd start to throw from emit()
+ // if it was messed up
+ if (typeof callback !== 'function')
+ throw new Error('When connecting signal must give a callback that is a function');
+
+ let id = this.#nextConnectionId;
+ this.#nextConnectionId += 1;
+
+ // this makes it O(n) in total connections to emit, but I think
+ // it's right to optimize for low memory and reentrancy-safety
+ // rather than speed
+ this.#signalConnections.push({
+ id,
+ name,
+ callback,
+ 'disconnected': false,
+ });
+ return id;
+ }
+
+ disconnect(id) {
+ let i;
+ let length = this.#signalConnections.length;
+ for (i = 0; i < length; ++i) {
+ let connection = this.#signalConnections[i];
+ if (connection.id === id) {
+ if (connection.disconnected)
+ throw new Error(`Signal handler id ${id} already disconnected`);
+
+ // set a flag to deal with removal during emission
+ connection.disconnected = true;
+ this.#signalConnections.splice(i, 1);
+
+ return;
+ }
+ }
+
+ throw new Error(`No signal connection ${id} found`);
+ }
+
+ signalHandlerIsConnected(id) {
+ const {length} = this.#signalConnections;
+ for (let i = 0; i < length; ++i) {
+ const connection = this.#signalConnections[i];
+ if (connection.id === id)
+ return !connection.disconnected;
+ }
+
+ return false;
+ }
+
+ disconnectAll() {
+ while (this.#signalConnections.length > 0)
+ this.disconnect(this.#signalConnections[0].id);
+ }
+
+ emit(name, ...args) {
+ // To deal with re-entrancy (removal/addition while
+ // emitting), we copy out a list of what was connected
+ // at emission start; and just before invoking each
+ // handler we check its disconnected flag.
+ let handlers = [];
+ let i;
+ let length = this.#signalConnections.length;
+ for (i = 0; i < length; ++i) {
+ let connection = this.#signalConnections[i];
+ if (connection.name === name)
+ handlers.push(connection);
+ }
+
+ // create arg array which is emitter + everything passed in except
+ // signal name. Would be more convenient not to pass emitter to
+ // the callback, but trying to be 100% consistent with GObject
+ // which does pass it in. Also if we pass in the emitter here,
+ // people don't create closures with the emitter in them,
+ // which would be a cycle.
+ let argArray = [this, ...args];
+
+ length = handlers.length;
+ for (i = 0; i < length; ++i) {
+ let connection = handlers[i];
+ if (!connection.disconnected) {
+ try {
+ // since we pass "null" for this, the global object will be used.
+ let ret = connection.callback.apply(null, argArray);
+
+ // if the callback returns true, we don't call the next
+ // signal handlers
+ if (ret === true)
+ break;
+ } catch (e) {
+ // just log any exceptions so that callbacks can't disrupt
+ // signal emission
+ logError(e, `Exception in callback for signal: ${name}`);
+ }
+ }
+ }
+ }
+}
diff --git a/modules/esm/repl.js b/modules/esm/repl.js
index 7dac78ef0..0ab9e059b 100644
--- a/modules/esm/repl.js
+++ b/modules/esm/repl.js
@@ -4,7 +4,13 @@
import GLib from 'gi://GLib';
let Gio;
-import {Ansi, Keycode} from './_repl/cliffy.js';
+import {emitKeys, CSI} from './_repl/utils.js';
+import {cursorTo} from './_repl/callbacks.js';
+
+import {EventEmitter} from './events.js';
+
+const cursorHide = CSI`?25l`;
+const cursorShow = CSI`?25h`;
const Console = import.meta.importSync('_consoleNative');
@@ -29,313 +35,331 @@ function toString(value) {
return `${value}`;
}
-class ReplInput {
- #inputHandler;
- #exitHandler;
- #exitWarning;
+/**
+ * @param {string} string
+ * @param {number} index
+ * @param {number} removeCount
+ * @param {string} replacement
+ * @returns {string}
+ */
+function StringSplice(string, index, removeCount = 0, replacement = '') {
+ return string.slice(0, index) + replacement + string.slice(index + removeCount);
+}
+export class Readline extends EventEmitter {
#prompt;
+ #input = '';
#cancelling = false;
- #cancellable = null;
/**
- * @param {object} _ _
- * @param {Gio.UnixOutputStream} _.stdin the input stream to treat as stdin
- * @param {Gio.UnixOutputStream} _.stdout the output stream to treat as stdout
- * @param {Gio.UnixOutputStream} _.stderr the output stream to treat as stderr
- * @param {boolean} _.enableColor whether to print ANSI color codes
+ * Store pending lines
+ *
+ * @example
+ * gjs > 'a pending line...
+ * ..... '
+ *
+ * @type {string[]}
*/
- constructor({stdin, stdout, stderr, enableColor}) {
- this.stdin = stdin;
- this.stdout = stdout;
- this.stderr = stderr;
- this.enableColor = enableColor;
+ #pendingInputLines = [];
- this.#prompt = this.#buildPrompt();
- this.#cancelling = false;
+ /**
+ * @param {object} _ _
+ * @param {string} _.prompt
+ */
+ constructor({prompt}) {
+ ({Gio} = imports.gi);
- /**
- * Store previously inputted lines
- *
- * @type {string[]}
- */
- this.history = [];
- /**
- * Store pending lines
- *
- * @example
- * gjs > 'a pending line...
- * ..... '
- *
- * @type {string[]}
- */
- this.pendingInputLines = [];
- /**
- * The current input buffer (in chars)
- *
- * @type {string[]}
- */
- this.currentInputChars = [];
-
- /**
- * The cursor's current column position.
- */
- this.cursorColumn = 0;
-
- this.#inputHandler =
- /**
- * @param {string} _input the inputted line or lines (separated by \n)
- */
- _input => { };
+ super();
- this.#exitWarning = false;
+ this.#prompt = prompt;
}
[Symbol.toStringTag]() {
- return 'Repl';
+ return 'Readline';
}
get cancelled() {
return this.#cancelling;
}
- /**
- * @param {string} pointer the pointer to prefix the line with
- */
- #buildPrompt(pointer = '>') {
- const renderedPrompt = `${pointer} `;
- const length = renderedPrompt.length;
+ get line() {
+ return this.#input;
+ }
- return {
- pointer,
- length,
- renderedPrompt,
- };
+ set line(value) {
+ this.#input = value;
}
- /**
- * @returns {string}
- */
- getValue() {
- if (this.historyIndex >= 0)
- return this.history[this.historyIndex].join('');
+ validate(input) {
+ return Console.isValid(input);
+ }
+ processLine() {
+ const {line} = this;
+ // Rebuild the input...
+ const js = [...this.#pendingInputLines, line].join('\n');
- return this.currentInputChars.join('');
- }
+ // Reset state...
+ this.#input = '';
- /**
- * @returns {string[]}
- */
- getEditableValue() {
- if (this.historyIndex > -1) {
- // TODO(ewlsh): This allows editing each history entry
- // 'in place'.
- return this.history[this.historyIndex];
+ // Only trigger input if this is a compilable unit...
+ if (this.validate(js)) {
+ // Reset lines before input is triggered
+ this.#pendingInputLines = [];
+ this.emit('line', js);
+ } else {
+ // Buffer the input until a compilable unit is found...
+ this.#pendingInputLines.push(line);
}
- return this.currentInputChars;
}
- editValue(editor) {
- if (this.historyIndex > -1) {
- this.history[this.historyIndex] = editor(this.history[this.historyIndex]);
- return this.history[this.historyIndex];
+ get inputPrompt() {
+ if (this.#pendingInputLines.length > 0) {
+ // Create a prefix like '... '
+ return ' '.padStart(4, '.');
}
- this.currentInputChars = editor(this.currentInputChars);
- return this.currentInputChars;
+ return this.#prompt;
}
- validate(input) {
- return Console.isValid(input);
+ print(output) {
+ }
+
+ render() {
+ }
+
+ prompt() {
+ this.#cancelling = false;
+ }
+
+ exit() {
+ }
+
+ cancel() {
+ this.#cancelling = true;
}
+}
+
+export class AsyncReadline extends Readline {
+ #exitWarning;
+
+ #parser;
+
+ #cancellable = null;
+
+ /**
+ * Store previously inputted lines
+ *
+ * @type {string[]}
+ */
+ #history = [];
+
+ /**
+ * The cursor's current column position.
+ */
+ #cursorColumn = 0;
+
+ /**
+ * @param {object} _ _
+ * @param {Gio.UnixOutputStream} _.stdin the input stream to treat as stdin
+ * @param {Gio.UnixOutputStream} _.stdout the output stream to treat as stdout
+ * @param {Gio.UnixOutputStream} _.stderr the output stream to treat as stderr
+ * @param {boolean} _.enableColor whether to print ANSI color codes
+ * @param _.prompt
+ */
+ constructor({stdin, stdout, stderr, enableColor, prompt}) {
+ super({prompt});
+
+ this.stdin = stdin;
+ this.stdout = stdout;
+ this.stderr = stderr;
+ this.enableColor = enableColor;
+
+
+ this.#parser = emitKeys(this.#onKeyPress.bind(this));
+ this.#parser.next();
+
- clear(lines = 1) {
- this.writeSync(Ansi.cursorLeft);
- this.writeSync(Ansi.eraseDown(lines));
+ this.#exitWarning = false;
+ }
+
+ get line() {
+ if (this.historyIndex > -1)
+ return this.#history[this.historyIndex];
+
+
+ return super.line;
+ }
+
+ set line(value) {
+ if (this.historyIndex > -1) {
+ this.#history[this.historyIndex] = value;
+ return;
+ }
+
+ super.line = value;
}
exit() {
if (this.#exitWarning) {
this.#exitWarning = false;
- this.#exitHandler?.();
+ this.emit('exit');
} else {
this.#exitWarning = true;
- this.writeSync('\n(To exit, press Ctrl+C again or Ctrl+D)\n');
- this.flush();
+ this.print('\n(To exit, press Ctrl+C again or Ctrl+D)\n');
}
}
historyUp() {
- if (this.historyIndex < this.history.length - 1) {
+ if (this.historyIndex < this.#history.length - 1) {
this.historyIndex++;
- this.cursorColumn = -1;
+ this.cursor = -1;
}
}
historyDown() {
if (this.historyIndex >= 0) {
this.historyIndex--;
- this.cursorColumn = -1;
+ this.cursor = -1;
}
}
moveCursorToBeginning() {
- this.cursorColumn = 0;
+ this.cursor = 0;
}
moveCursorToEnd() {
- this.cursorColumn = this.getValue().length;
+ this.cursor = this.line.length;
}
moveCursorLeft() {
- if (this.cursorColumn > 0)
- this.cursorColumn--;
+ this.cursor--;
}
moveCursorRight() {
- if (this.cursorColumn < this.getValue().length)
- this.cursorColumn++;
+ this.cursor++;
}
addChar(char) {
- const editableValue = this.getEditableValue();
- editableValue.splice(this.cursorColumn, 0, char);
-
- this.cursorColumn++;
+ this.line = StringSplice(this.line, this.cursor, 0, char);
+ this.moveCursorRight();
}
deleteChar() {
- const editableValue = this.getEditableValue();
- if (this.cursorColumn > 0 && editableValue.length > 0)
- editableValue.splice(this.cursorColumn - 1, 1);
+ const {line} = this;
- this.moveCursorLeft();
+ if (line.length > 0 && this.cursor > 0) {
+ const x = StringSplice(line, this.cursor - 1, 1);
+
+ this.line = x;
+ this.moveCursorLeft();
+ }
}
deleteCharRightOrClose() {
- const editableValue = this.getEditableValue();
- if (this.cursorColumn < editableValue.length - 1)
- editableValue.splice(this.cursorColumn, 1);
+ const {line} = this;
+
+ if (this.cursor < line.length - 1)
+ this.line = StringSplice(this.line, this.cursor, 1);
else
this.exit();
}
deleteToBeginning() {
- const editableValue = this.getEditableValue();
-
- editableValue.splice(0, this.cursorColumn);
+ this.line = StringSplice(this.line, 0, this.cursor);
}
deleteToEnd() {
- const editableValue = this.getEditableValue();
-
- editableValue.splice(this.cursorColumn);
+ this.line = StringSplice(this.line, this.cursor);
}
/**
* Adapted from lib/readline.js in Node.js
*/
- _deleteWordLeft() {
- this.editValue(value => {
- if (this.cursorColumn > 0) {
- // Reverse the string and match a word near beginning
- // to avoid quadratic time complexity
- let leading = value.slice(0, this.cursorColumn);
- const reversed = [...leading].reverse().join('');
- const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
- leading = leading.slice(0,
- leading.length - match[0].length);
- value = leading.concat(value.slice(this.cursorColumn));
- this.cursorColumn = leading.length;
-
- return value;
- }
- });
+ deleteWordLeft() {
+ const {line} = this;
+
+ if (this.cursor > 0) {
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ let leading = line.slice(0, this.cursor);
+ const reversed = [...leading].reverse().join('');
+ const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+ leading = leading.slice(0,
+ leading.length - match[0].length);
+ this.line = leading.concat(line.slice(this.cursor));
+ this.cursor = leading.length;
+ }
}
/**
* Adapted from lib/readline.js in Node.js
*/
- _deleteWordRight() {
- this.editValue(value => {
- if (this.currentInputChars.length > 0 && this.cursorColumn < value.length) {
- const trailing = value.slice(this.cursorColumn).join('');
- const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
- value = value.slice(0, this.cursorColumn).concat(
- trailing.slice(match[0].length));
- return value;
- }
- });
+ deleteWordRight() {
+ const {line} = this;
+
+ if (line.length > 0 && this.cursor < line.length) {
+ const trailing = line.slice(this.cursor);
+ const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
+ this.line = line.slice(0, this.cursor).concat(
+ trailing.slice(match[0].length));
+ }
}
/**
* Adapted from lib/readline.js in Node.js
*/
- _wordLeft() {
- if (this.cursorColumn > 0) {
- const value = this.getValue();
+ wordLeft() {
+ const {line} = this;
+ if (this.cursor > 0) {
// Reverse the string and match a word near beginning
// to avoid quadratic time complexity
- const leading = value.slice(0, this.cursorColumn);
- const reversed = Array.from(leading).reverse().join('');
+ const leading = line.slice(0, this.cursor);
+ const reversed = [...leading].reverse().join('');
const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
- this.cursorColumn -= match[0].length;
- this.cursorColumn = Math.max(0, this.cursorColumn);
+ this.cursor -= match[0].length;
+ this.cursor = Math.max(0, this.cursor);
}
}
/**
* Adapted from lib/readline.js in Node.js
*/
- _wordRight() {
- const value = this.getValue();
+ wordRight() {
+ const {line} = this;
- if (this.cursorColumn < value.length) {
- const trailing = value.slice(this.cursorColumn);
+ if (this.cursor < line.length) {
+ const trailing = line.slice(this.cursor);
const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
- this.cursorColumn += match[0].length;
+ this.cursor += match[0].length;
}
}
- get isRaw() {
- return true;
- }
-
processLine() {
- const value = this.getValue();
- // Rebuild the input...
- const js = [...this.pendingInputLines, value].join('\n');
+ const {line} = this;
- // Reset state...
- this.history.unshift(value.split(''));
+ this.#history.unshift(line);
this.historyIndex = -1;
- this.currentInputChars = [];
- this.cursorColumn = 0;
-
- // In raw mode we need to manually write a
- // new line...
- if (this.isRaw) {
- // Append the new line...
- this.writeSync('\n');
- this.flush();
- }
+ this.#exitWarning = false;
+ this.cursor = 0;
+ this.#write('\n');
- // Only trigger input if this is a compilable unit...
- if (this.validate(js)) {
- // Reset lines before input is triggered
- this.pendingInputLines = [];
- this.#exitWarning = false;
- this.#inputHandler?.(js);
- } else {
- // Buffer the input until a compilable unit is found...
- this.pendingInputLines.push(value);
- }
+ super.processLine();
+ }
+
+ #onKeyPress(sequence, key) {
+ this.#processKey(key);
+
+ if (!this.cancelled)
+ this.render();
}
- handleEvent(key) {
+ #processKey(key) {
+ if (!key.sequence)
+ return;
+
if (key.ctrl && !key.meta && !key.shift) {
switch (key.name) {
case 'c':
@@ -379,31 +403,31 @@ class ReplInput {
return;
case 'w':
case 'backspace':
- this._deleteWordLeft();
+ this.deleteWordLeft();
return;
case 'delete':
- this._deleteWordRight();
+ this.deleteWordRight();
return;
case 'left':
- this._wordLeft();
+ this.wordLeft();
return;
case 'right':
- this._wordRight();
+ this.wordRight();
return;
}
} else if (key.meta && !key.shift) {
switch (key.name) {
case 'd':
- this._deleteWordRight();
+ this.deleteWordRight();
return;
case 'backspace':
- this._deleteWordLeft();
+ this.deleteWordLeft();
return;
case 'b':
- this._wordLeft();
+ this.wordLeft();
return;
case 'f':
- this._wordRight();
+ this.wordRight();
return;
}
}
@@ -432,203 +456,184 @@ class ReplInput {
this.addChar(key.sequence);
}
- updateInputIndex(value) {
- if (this.cursorColumn === -1)
- this.cursorColumn = value.length;
-
+ /**
+ * @param {number} length
+ */
+ set cursor(length) {
+ if (length < 0) {
+ this.#cursorColumn = 0;
+ return;
+ }
// Ensure the input index isn't longer than the content...
- this.cursorColumn = Math.min(this.cursorColumn, value.length);
+ this.#cursorColumn = Math.min(this.line.length, length);
}
- getPrompt() {
- if (this.pendingInputLines.length > 0) {
- // Create a prefix like '... '
- return ' '.padStart(4, '.');
- }
-
- return this.#prompt.renderedPrompt;
+ get cursor() {
+ return this.#cursorColumn;
}
render() {
- const value = this.getValue();
-
// Prevent the cursor from flashing while we render...
- this.writeSync(Ansi.cursorHide);
- this.clear();
+ this.#write(cursorHide);
+
+ const {inputPrompt, line} = this;
- const prompt = this.getPrompt();
- this.writeSync(prompt + value);
+ this.#write(
+ cursorTo(0),
+ CSI.kClearScreenDown,
+ inputPrompt,
+ line,
+ cursorTo(inputPrompt.length + this.cursor),
+ cursorShow
+ );
- this.updateInputIndex(value);
- this.writeSync(Ansi.cursorTo(prompt.length + this.cursorColumn + 1));
- this.writeSync(Ansi.cursorShow);
- this.flush();
+ this.emit('render');
}
- flush() {
+ #write(...strings) {
+ const bytes = new TextEncoder().encode(strings.join(''));
+
+ this.stdout.write_bytes(bytes, null);
this.stdout.flush(null);
}
/**
- * @param {Uint8Array | string} buffer a string or Uint8Array to write to stdout
+ * @param {string[]} strings strings to write to stdout
*/
- writeSync(buffer) {
- if (typeof buffer === 'string')
- buffer = new TextEncoder().encode(buffer);
- this.stdout.write_bytes(buffer, null);
+ print(...strings) {
+ this.#write(...strings, '\n');
}
+ /**
+ * @param {Uint8Array} bytes
+ * @returns {void}
+ */
handleInput(bytes) {
if (bytes.length === 0)
return;
- for (const event of Keycode.parse(bytes)) {
- this.handleEvent(event);
+ const input = String.fromCharCode(...bytes.values());
- if (this.#cancelling)
- break;
+ for (const byte of input) {
+ this.#parser.next(byte);
- this.render();
+ if (this.cancelled)
+ break;
}
}
#asyncReadHandler(stream, result) {
- this.#cancellable = null;
-
- if (this.#cancelling)
- return;
-
if (result) {
- const gbytes = stream.read_bytes_finish(result);
+ try {
+ const gbytes = stream.read_bytes_finish(result);
+
+ this.handleInput(gbytes.toArray());
+ } catch (error) {
+ if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ console.error(error);
+ imports.system.exit(1);
- this.handleInput(gbytes.toArray());
+ return;
+ }
+ }
}
+ if (this.cancelled)
+ return;
+
this.#cancellable = new Gio.Cancellable();
stream.read_bytes_async(8, 0, this.#cancellable, this.#asyncReadHandler.bind(this));
}
cancel() {
- this.#cancelling = true;
+ super.cancel();
this.#cancellable?.cancel();
this.#cancellable = null;
- this.writeSync('\n');
- this.flush();
+ this.#write('\n');
}
- read() {
+ prompt() {
+ super.prompt();
this.render();
// Start the async read loop...
this.#asyncReadHandler(this.stdin);
}
-
- /**
- *
- * @param {(input: string) => void} inputHandler a callback when new lines are processed
- */
- prompt(inputHandler) {
- this.#inputHandler = inputHandler;
- this.#cancelling = false;
- this.read();
- }
-
- onExit(exitHandler) {
- this.#exitHandler = exitHandler;
- }
}
-class FallbackReplInput extends ReplInput {
- constructor() {
- super({});
+export class SyncReadline extends Readline {
+ constructor({prompt}) {
+ super({prompt});
}
- read() {
+ prompt() {
while (!this.cancelled) {
- const prompt = this.getPrompt();
- this.editValue(() => {
- try {
- return Console.interact(prompt).split('');
- } catch {
- return '';
- }
- });
+ const {inputPrompt} = this;
+
+ try {
+ this.line = Console.interact(inputPrompt).split('');
+ } catch {
+ this.line = '';
+ }
+
this.processLine();
}
}
- get isRaw() {
- return false;
+ print(output) {
+ print(output);
}
-
- writeSync(buffer) {
- if (buffer instanceof Uint8Array)
- buffer = new TextDecoder().decode(buffer);
- print(buffer);
- }
-
- flush() { }
}
export class Repl {
#lineNumber = 0;
- #isRaw = false;
+ #isAsync = false;
+ /** @type {boolean} */
#supportsColor;
- #mainloop;
+ /** @type {string} */
#version;
+ #mainloop = false;
+
constructor() {
({Gio} = imports.gi);
this.#version = imports.system.versionString;
- }
- get lineNumber() {
- return this.#lineNumber;
- }
+ try {
+ this.#supportsColor = GLib.log_writer_supports_color(1) && GLib.getenv('NO_COLOR') === null;
+ } catch {
+ this.#supportsColor ||= false;
+ }
- get isRaw() {
- return this.#isRaw;
+ try {
+ this.#isAsync &&= GLib.getenv('GJS_REPL_USE_FALLBACK') !== 'true';
+ this.#isAsync = 'UnixInputStream' in Gio && 'UnixOutputStream' in Gio;
+ this.#isAsync &&= Console.enableRawMode();
+ } catch {
+ this.#isAsync = false;
+ }
}
- #checkEnvironment() {
- this.#supportsColor = GLib.log_writer_supports_color(1);
- this.#supportsColor &&= GLib.getenv('NO_COLOR') === null;
-
- let hasUnixStreams = 'UnixInputStream' in Gio;
- hasUnixStreams = hasUnixStreams && 'UnixOutputStream' in Gio;
-
- if (!hasUnixStreams)
- return false;
-
- const noMainLoop = GLib.getenv('GJS_REPL_NO_MAINLOOP');
- // TODO: Environment variable for testing.
- if (noMainLoop && noMainLoop === 'true')
- return false;
-
- return true;
+ [Symbol.toStringTag]() {
+ return 'Repl';
}
- #registerInputHandler() {
- this.#print(`GJS v${this.#version}`);
+ get lineNumber() {
+ return this.#lineNumber;
+ }
- // Start accepting input and rendering...
- this.input.prompt(lines => {
- if (lines.trim().startsWith('exit()'))
- this.exit();
- else
- this.evaluate(lines);
- });
+ get supportsColor() {
+ return this.#supportsColor;
}
#print(string) {
- this.input.writeSync(`${string}${this.#isRaw ? '\n' : ''}`);
- this.input.flush();
+ this.input.print(`${string}`);
}
#evaluateInternal(lines) {
@@ -672,90 +677,68 @@ export class Repl {
this.#printError(error);
}
+ #start() {
+ this.input.print(`GJS v${this.#version}`);
- run() {
- if (!this.#checkEnvironment()) {
- this.input = new FallbackReplInput();
+ this.input.connect('line', (_, line) => {
+ if (typeof line === 'string' && line.trim().startsWith('exit()'))
+ this.exit();
+ else
+ this.evaluate(line);
+ });
- this.#registerInputHandler();
- return;
- }
+ this.input.connect('exit', () => {
+ this.exit();
+ });
- try {
- this.#isRaw = Console.enableRawMode();
+ this.input.prompt();
+ }
- if (!this.#isRaw) {
- this.input = new FallbackReplInput();
+ start() {
+ if (!this.#isAsync) {
+ this.input = new SyncReadline({prompt: '> '});
- this.#registerInputHandler();
- return;
- }
+ this.#start();
+ return;
+ }
+
+ try {
const stdin = Gio.UnixInputStream.new(0, false);
- const stdout = Gio.UnixOutputStream.new(1, false);
+ const stdout = new Gio.BufferedOutputStream({
+ baseStream: Gio.UnixOutputStream.new(1, false),
+ closeBaseStream: false,
+ autoGrow: true,
+ });
const stderr = Gio.UnixOutputStream.new(2, false);
- this.input = new ReplInput({
+ this.input = new AsyncReadline({
stdin,
stdout,
stderr,
enableColor: this.#supportsColor,
+ prompt: '> ',
});
- this.input.onExit(() => {
- this.exit();
- });
-
- this.#registerInputHandler();
-
- // Install our default mainloop...
- this.replaceMainLoop(() => {
- imports.mainloop.run('repl');
- }, () => {
- imports.mainloop.quit('repl');
- });
-
- let mainloop = this.#mainloop;
- while (mainloop) {
- const [start] = mainloop;
-
- start();
+ this.#start();
- mainloop = this.#mainloop;
- }
+ this.#mainloop = true;
+ imports.mainloop.run('repl');
} finally {
Console.disableRawMode();
}
}
exit() {
- this.input.cancel();
-
- const mainloop = this.#mainloop;
- this.#mainloop = null;
-
- if (mainloop) {
- const [, quit] = mainloop;
-
- quit?.();
- }
- }
-
- replaceMainLoop(start, quit = () => {
- // Force an exit if a user doesn't define their
- // replacement mainloop's quit function.
- imports.system.exit(1);
- }) {
- if (!(this.input instanceof ReplInput))
- return;
-
- const mainloop = this.#mainloop;
- this.#mainloop = [start, quit];
-
- if (mainloop) {
- const [, previousQuit] = mainloop;
+ try {
+ this.input.cancel();
- previousQuit?.();
+ if (this.#mainloop)
+ imports.mainloop.quit('repl');
+ } catch {
+ // Force an exit if a user doesn't define their
+ // replacement mainloop's quit function.
+ imports.system.exit(1);
}
}
}
diff --git a/modules/script/console.js b/modules/script/console.js
index 82ae75cc8..d80b699c1 100644
--- a/modules/script/console.js
+++ b/modules/script/console.js
@@ -8,5 +8,5 @@ var Repl = null;
function interact() {
const repl = new Repl();
- repl.run();
+ repl.start();
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]