[gjs/ewlsh/nova-repl: 3/3] Implement non-blocking Repl with Mainloop
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/nova-repl: 3/3] Implement non-blocking Repl with Mainloop
- Date: Sun, 30 Jan 2022 06:26:33 +0000 (UTC)
commit eda36836fafacd48894616300089420b1b0b3976
Author: Evan Welsh <contact evanwelsh com>
Date: Sat Jan 29 19:08:13 2022 -0800
Implement non-blocking Repl with Mainloop
Implement Repl with Node.js readline#Implement non-blocking Repl with Mainloop
gjs/console.cpp | 20 +-
installed-tests/js/.eslintrc.yml | 1 +
installed-tests/js/meson.build | 1 +
installed-tests/js/testRepl.js | 137 +++++++
js.gresource.xml | 7 +
modules/console.cpp | 258 +++++--------
modules/console.h | 4 +-
modules/esm/_bootstrap/default.js | 3 +
modules/esm/_bootstrap/repl.js | 10 +
modules/esm/_repl/callbacks.js | 99 +++++
modules/esm/_repl/primordials.js | 36 ++
modules/esm/_repl/utils.js | 419 +++++++++++++++++++++
modules/esm/repl.js | 746 ++++++++++++++++++++++++++++++++++++++
modules/modules.cpp | 3 +-
modules/script/console.js | 12 +
15 files changed, 1575 insertions(+), 181 deletions(-)
---
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 49c822992..d51a98aca 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -30,6 +30,7 @@ static char *command = NULL;
static gboolean print_version = false;
static gboolean print_js_version = false;
static gboolean debugging = false;
+static gboolean use_interactive_repl = false;
static gboolean exec_as_module = false;
static bool enable_profiler = false;
@@ -43,6 +44,8 @@ static GOptionEntry entries[] = {
{ "command", 'c', 0, G_OPTION_ARG_STRING, &command, "Program passed in as a string", "COMMAND" },
{ "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &coverage_prefixes, "Add the prefix PREFIX to
the list of files to generate coverage info for", "PREFIX" },
{ "coverage-output", 0, 0, G_OPTION_ARG_STRING, &coverage_output_path, "Write coverage output to a
directory DIR. This option is mandatory when using --coverage-prefix", "DIR", },
+ { "interactive", 'i', 0, G_OPTION_ARG_NONE, &use_interactive_repl,
+ "Start the interactive repl"},
{ "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, &include_path, "Add the directory DIR to the list
of directories to search for js files.", "DIR" },
{ "module", 'm', 0, G_OPTION_ARG_NONE, &exec_as_module, "Execute the file as a module." },
{ "profile", 0, G_OPTION_FLAG_OPTIONAL_ARG | G_OPTION_FLAG_FILENAME,
@@ -257,6 +260,7 @@ main(int argc, char **argv)
print_js_version = false;
debugging = false;
exec_as_module = false;
+ use_interactive_repl = false;
g_option_context_set_ignore_unknown_options(context, false);
g_option_context_set_help_enabled(context, true);
if (!g_option_context_parse_strv(context, &gjs_argv, &error)) {
@@ -293,9 +297,19 @@ main(int argc, char **argv)
exit(1);
}
- script = g_strdup("const Console = imports.console; Console.interact();");
- len = strlen(script);
- filename = "<stdin>";
+ if (use_interactive_repl) {
+ script = nullptr;
+ exec_as_module = true;
+ filename =
+ "resource:///org/gnome/gjs/modules/esm/_bootstrap/repl.js";
+ interactive_mode = true;
+ } else {
+ script = g_strdup(
+ "const Console = imports.console; Console.interact();");
+ filename = "<stdin>";
+ len = strlen(script);
+ }
+
program_name = gjs_argv[0];
interactive_mode = true;
} else {
diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml
index d10c2c904..11afa7d43 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -33,6 +33,7 @@ overrides:
- testEncoding.js
- testEvents.js
- testGLibLogWriter.js
+ - testRepl.js
- testTimers.js
- modules/importmeta.js
- modules/exports.js
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index 5f20bde61..24f0998b3 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',
'Events',
diff --git a/installed-tests/js/testRepl.js b/installed-tests/js/testRepl.js
new file mode 100644
index 000000000..35b40fdbb
--- /dev/null
+++ b/installed-tests/js/testRepl.js
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Evan Welsh <contact evanwelsh com>
+
+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, 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);
+ });
+});
diff --git a/js.gresource.xml b/js.gresource.xml
index e3eab651a..dc6d37407 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -9,6 +9,7 @@
<!-- ESM-based modules -->
<file>modules/esm/_bootstrap/default.js</file>
+ <file>modules/esm/_bootstrap/repl.js</file>
<file>modules/esm/_encoding/encoding.js</file>
<file>modules/esm/_encoding/encodingMap.js</file>
@@ -16,11 +17,16 @@
<file>modules/esm/_timers.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/events.js</file>
<file>modules/esm/gettext.js</file>
<file>modules/esm/console.js</file>
<file>modules/esm/gi.js</file>
+ <file>modules/esm/repl.js</file>
<file>modules/esm/system.js</file>
<!-- Script-based Modules -->
@@ -34,6 +40,7 @@
<file>modules/script/byteArray.js</file>
<file>modules/script/cairo.js</file>
+ <file>modules/script/console.js</file>
<file>modules/script/gettext.js</file>
<file>modules/script/lang.js</file>
<file>modules/script/_legacy.js</file>
diff --git a/modules/console.cpp b/modules/console.cpp
index c21942490..e03cd1453 100644
--- a/modules/console.cpp
+++ b/modules/console.cpp
@@ -3,54 +3,35 @@
// SPDX-License-Identifier: MPL-1.1 OR GPL-2.0-or-later OR LGPL-2.1-or-later
// SPDX-FileCopyrightText: 1998 Netscape Communications Corporation
-#include <config.h> // for HAVE_READLINE_READLINE_H
-
-#ifdef HAVE_SIGNAL_H
-# include <setjmp.h>
-# include <signal.h>
-# ifdef _WIN32
-# define sigjmp_buf jmp_buf
-# define siglongjmp(e, v) longjmp (e, v)
-# define sigsetjmp(v, m) setjmp (v)
-# endif
-#endif
-
-#ifdef HAVE_READLINE_READLINE_H
-# include <stdio.h> // include before readline/readline.h
-
-# include <readline/history.h>
-# include <readline/readline.h>
-#endif
+#include <config.h>
#include <string>
#include <glib.h>
-#include <glib/gprintf.h> // for g_fprintf
+#include <stdio.h>
#include <js/CallArgs.h>
#include <js/CompilationAndEvaluation.h>
#include <js/CompileOptions.h>
#include <js/ErrorReport.h>
#include <js/Exception.h>
+#include <js/PropertySpec.h>
#include <js/RootingAPI.h>
#include <js/SourceText.h>
#include <js/TypeDecls.h>
-#include <js/Warnings.h>
+#include <js/Utility.h>
#include <jsapi.h> // for JS_IsExceptionPending, Exce...
-#include "gjs/atoms.h"
#include "gjs/context-private.h"
+#include "gjs/jsapi-util-args.h"
#include "gjs/jsapi-util.h"
#include "modules/console.h"
+#include "util/console.h"
namespace mozilla {
union Utf8Unit;
}
-static void gjs_console_warning_reporter(JSContext*, JSErrorReport* report) {
- JS::PrintError(stderr, report, /* reportWarnings = */ true);
-}
-
/* Based on js::shell::AutoReportException from SpiderMonkey. */
class AutoReportException {
JSContext *m_cx;
@@ -90,69 +71,20 @@ public:
}
};
-
-// Adapted from https://stackoverflow.com/a/17035073/172999
-class AutoCatchCtrlC {
-#ifdef HAVE_SIGNAL_H
- void (*m_prev_handler)(int);
-
- static void handler(int signal) {
- if (signal == SIGINT)
- siglongjmp(jump_buffer, 1);
- }
-
- public:
- static sigjmp_buf jump_buffer;
-
- AutoCatchCtrlC() {
- m_prev_handler = signal(SIGINT, &AutoCatchCtrlC::handler);
- }
-
- ~AutoCatchCtrlC() {
- if (m_prev_handler != SIG_ERR)
- signal(SIGINT, m_prev_handler);
- }
-
- void raise_default() {
- if (m_prev_handler != SIG_ERR)
- signal(SIGINT, m_prev_handler);
- raise(SIGINT);
- }
-#endif // HAVE_SIGNAL_H
-};
-
-#ifdef HAVE_SIGNAL_H
-sigjmp_buf AutoCatchCtrlC::jump_buffer;
-#endif // HAVE_SIGNAL_H
-
[[nodiscard]] static bool gjs_console_readline(char** bufp,
const char* prompt) {
-#ifdef HAVE_READLINE_READLINE_H
- char *line;
- line = readline(prompt);
- if (!line)
- return false;
- if (line[0] != '\0')
- add_history(line);
- *bufp = line;
-#else // !HAVE_READLINE_READLINE_H
char line[256];
fprintf(stdout, "%s", prompt);
fflush(stdout);
if (!fgets(line, sizeof line, stdin))
return false;
*bufp = g_strdup(line);
-#endif // !HAVE_READLINE_READLINE_H
return true;
}
-/* Return value of false indicates an uncatchable exception, rather than any
- * exception. (This is because the exception should be auto-printed around the
- * invocation of this function.)
- */
-[[nodiscard]] static bool gjs_console_eval_and_print(JSContext* cx,
- const std::string& bytes,
- int lineno) {
+[[nodiscard]] static bool gjs_console_eval(JSContext* cx,
+ const std::string& bytes, int lineno,
+ JS::MutableHandleValue result) {
JS::SourceText<mozilla::Utf8Unit> source;
if (!source.init(cx, bytes.c_str(), bytes.size(),
JS::SourceOwnership::Borrowed))
@@ -161,116 +93,86 @@ sigjmp_buf AutoCatchCtrlC::jump_buffer;
JS::CompileOptions options(cx);
options.setFileAndLine("typein", lineno);
- JS::RootedValue result(cx);
- if (!JS::Evaluate(cx, options, source, &result)) {
- if (!JS_IsExceptionPending(cx))
- return false;
- }
+ JS::RootedValue eval_result(cx);
+ if (!JS::Evaluate(cx, options, source, &eval_result))
+ return false;
GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
gjs->schedule_gc_if_needed();
- if (result.isUndefined())
+ result.set(eval_result);
+ return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_console_interact(JSContext* context, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JS::RootedObject global(context, gjs_get_import_global(context));
+
+ JS::UniqueChars prompt;
+ if (!gjs_parse_call_args(context, "interact", args, "s", "prompt", &prompt))
+ return false;
+
+ GjsAutoChar buffer;
+ if (!gjs_console_readline(buffer.out(), prompt.get())) {
return true;
+ }
- g_fprintf(stdout, "%s\n", gjs_value_debug_string(cx, result).c_str());
+ return gjs_string_from_utf8(context, buffer, args.rval());
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_console_enable_raw_mode(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ if (!gjs_parse_call_args(cx, "enableRawMode", args, ""))
+ return false;
+
+ args.rval().setBoolean(Gjs::Console::enable_raw_mode());
return true;
}
GJS_JSAPI_RETURN_CONVENTION
-static bool
-gjs_console_interact(JSContext *context,
- unsigned argc,
- JS::Value *vp)
-{
- JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
- bool eof, exit_warning;
- JS::RootedObject global(context, gjs_get_import_global(context));
- char* temp_buf;
+static bool gjs_console_disable_raw_mode(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ if (!gjs_parse_call_args(cx, "disableRawMode", args, ""))
+ return false;
+
+ args.rval().setBoolean(Gjs::Console::disable_raw_mode());
+ return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_console_eval_js(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JS::UniqueChars expr;
int lineno;
- int startline;
-
-#ifndef HAVE_READLINE_READLINE_H
- int rl_end = 0; // nonzero if using readline and any text is typed in
-#endif
-
- JS::SetWarningReporter(context, gjs_console_warning_reporter);
-
- AutoCatchCtrlC ctrl_c;
-
- // Separate initialization from declaration because of possible overwriting
- // when siglongjmp() jumps into this function
- eof = exit_warning = false;
- temp_buf = nullptr;
- lineno = 1;
- do {
- /*
- * Accumulate lines until we get a 'compilable unit' - one that either
- * generates an error (before running out of source) or that compiles
- * cleanly. This should be whenever we get a complete statement that
- * coincides with the end of a line.
- */
- startline = lineno;
- std::string buffer;
- do {
-#ifdef HAVE_SIGNAL_H
- // sigsetjmp() returns 0 if control flow encounters it normally, and
- // nonzero if it's been jumped to. In the latter case, use a while
- // loop so that we call sigsetjmp() a second time to reinit the jump
- // buffer.
- while (sigsetjmp(AutoCatchCtrlC::jump_buffer, 1) != 0) {
- g_fprintf(stdout, "\n");
- if (buffer.empty() && rl_end == 0) {
- if (!exit_warning) {
- g_fprintf(stdout,
- "(To exit, press Ctrl+C again or Ctrl+D)\n");
- exit_warning = true;
- } else {
- ctrl_c.raise_default();
- }
- } else {
- exit_warning = false;
- }
- buffer.clear();
- startline = lineno = 1;
- }
-#endif // HAVE_SIGNAL_H
-
- if (!gjs_console_readline(
- &temp_buf, startline == lineno ? "gjs> " : ".... ")) {
- eof = true;
- break;
- }
- buffer += temp_buf;
- buffer += "\n";
- g_free(temp_buf);
- lineno++;
- } while (!JS_Utf8BufferIsCompilableUnit(context, global, buffer.c_str(),
- buffer.size()));
-
- bool ok;
- {
- AutoReportException are(context);
- ok = gjs_console_eval_and_print(context, buffer, startline);
- }
- exit_warning = false;
-
- GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
- ok = gjs->run_jobs_fallible() && ok;
-
- if (!ok) {
- /* If this was an uncatchable exception, throw another uncatchable
- * exception on up to the surrounding JS::Evaluate() in main(). This
- * happens when you run gjs-console and type imports.system.exit(0);
- * at the prompt. If we don't throw another uncatchable exception
- * here, then it's swallowed and main() won't exit. */
- return false;
- }
- } while (!eof);
+ if (!gjs_parse_call_args(cx, "eval", args, "si", "expression", &expr,
+ "lineNumber", &lineno))
+ return false;
+
+ return gjs_console_eval(cx, std::string(expr.get()), lineno, args.rval());
+}
- g_fprintf(stdout, "\n");
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_console_is_valid_js(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JS::RootedString str(cx);
+ if (!gjs_parse_call_args(cx, "isValid", args, "S", "code", &str))
+ return false;
- argv.rval().setUndefined();
+ JS::UniqueChars code;
+ size_t code_len;
+ if (!gjs_string_to_utf8_n(cx, str, &code, &code_len))
+ return false;
+
+ JS::RootedObject global(cx, gjs_get_import_global(cx));
+
+ args.rval().setBoolean(
+ JS_Utf8BufferIsCompilableUnit(cx, global, code.get(), code_len));
return true;
}
@@ -291,9 +193,15 @@ static bool gjs_console_clear_terminal(JSContext* cx, unsigned argc,
}
static JSFunctionSpec console_module_funcs[] = {
+ JS_FN("interact", gjs_console_interact, 1, GJS_MODULE_PROP_FLAGS),
+ JS_FN("enableRawMode", gjs_console_enable_raw_mode, 0,
+ GJS_MODULE_PROP_FLAGS),
+ JS_FN("disableRawMode", gjs_console_disable_raw_mode, 0,
+ GJS_MODULE_PROP_FLAGS),
+ JS_FN("eval", gjs_console_eval_js, 2, GJS_MODULE_PROP_FLAGS),
+ JS_FN("isValid", gjs_console_is_valid_js, 1, GJS_MODULE_PROP_FLAGS),
JS_FN("clearTerminal", gjs_console_clear_terminal, 1,
GJS_MODULE_PROP_FLAGS),
- JS_FN("interact", gjs_console_interact, 1, GJS_MODULE_PROP_FLAGS),
JS_FS_END,
};
diff --git a/modules/console.h b/modules/console.h
index 73ed4c0ee..7df6a02d2 100644
--- a/modules/console.h
+++ b/modules/console.h
@@ -12,7 +12,7 @@
#include "gjs/macros.h"
GJS_JSAPI_RETURN_CONVENTION
-bool gjs_define_console_stuff(JSContext *context,
- JS::MutableHandleObject module);
+bool gjs_define_console_private_stuff(JSContext* context,
+ JS::MutableHandleObject module);
#endif // MODULES_CONSOLE_H_
diff --git a/modules/esm/_bootstrap/default.js b/modules/esm/_bootstrap/default.js
index ff1f28bfc..24abd76fd 100644
--- a/modules/esm/_bootstrap/default.js
+++ b/modules/esm/_bootstrap/default.js
@@ -9,3 +9,6 @@ import '_encoding/encoding';
import 'console';
// Bootstrap the Timers API
import '_timers';
+
+// Install the Repl constructor for Console.interact()
+import 'repl';
diff --git a/modules/esm/_bootstrap/repl.js b/modules/esm/_bootstrap/repl.js
new file mode 100644
index 000000000..cb8e98eb0
--- /dev/null
+++ b/modules/esm/_bootstrap/repl.js
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+import {Repl} from 'repl';
+
+const repl = new Repl();
+
+globalThis.repl = repl;
+
+repl.start();
diff --git a/modules/esm/_repl/callbacks.js b/modules/esm/_repl/callbacks.js
new file mode 100644
index 000000000..37910e03f
--- /dev/null
+++ b/modules/esm/_repl/callbacks.js
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: Node.js contributors. All rights reserved.
+
+/* eslint-disable */
+
+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..cc4f51ea4
--- /dev/null
+++ b/modules/esm/_repl/primordials.js
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Evan Welsh <contact evanwelsh com>
+
+/**
+ * @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..9934093b9
--- /dev/null
+++ b/modules/esm/_repl/utils.js
@@ -0,0 +1,419 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: Node.js contributors. All rights reserved.
+
+/* eslint-disable */
+
+import {primordials} from './primordials.js';
+
+/**
+ * @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
+ */
+
+// Copied 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;
+
+const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
+const kEscape = '\x1b';
+const kSubstringSearch = Symbol('kSubstringSearch');
+
+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
+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;
+}
+
+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 */
+ // stream.emit('keypress', escaped ? undefined : s, key);
+ // GJS:
+ callback(escaped ? undefined : s, key);
+ } else if (charLengthAt(s, 0) === s.length) {
+ /* Single unnamed character, e.g. "." */
+ // stream.emit('keypress', s, key);
+ // GJS:
+ callback(s, key);
+ }
+ /* Unrecognized or broken escape sequence, don't emit anything */
+ }
+}
+
+// This runs in O(n log n).
+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/repl.js b/modules/esm/repl.js
new file mode 100644
index 000000000..31cbcb6a7
--- /dev/null
+++ b/modules/esm/repl.js
@@ -0,0 +1,746 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+import GLib from 'gi://GLib';
+let Gio;
+
+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');
+
+// TODO: Integrate with new printer once it is merged...
+
+/**
+ * @param {any} value any valid JavaScript value to stringify
+ */
+function toString(value) {
+ if (typeof value === 'function')
+ return value.toString();
+
+ if (typeof value === 'object') {
+ // JSON will error if the object
+ // has circular references or cannot
+ // be converted.
+ try {
+ return JSON.stringify(value);
+ } catch {
+ }
+ }
+ return `${value}`;
+}
+
+/**
+ * @param {string} string the string to splice
+ * @param {number} index the index to start removing characters at
+ * @param {number} removeCount how many characters to remove
+ * @param {string} replacement a string to replace the removed characters with
+ * @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;
+
+ /**
+ * Store pending lines
+ *
+ * @example
+ * gjs > 'a pending line...
+ * ..... '
+ *
+ * @type {string[]}
+ */
+ #pendingInputLines = [];
+
+ /**
+ * @param {object} options _
+ * @param {string} options.prompt the prompt to print prior to the line
+ */
+ constructor({prompt}) {
+ ({Gio} = imports.gi);
+
+ super();
+
+ this.#prompt = prompt;
+ }
+
+ [Symbol.toStringTag]() {
+ return 'Readline';
+ }
+
+ get cancelled() {
+ return this.#cancelling;
+ }
+
+ get line() {
+ return this.#input;
+ }
+
+ set line(value) {
+ this.#input = value;
+ }
+
+ validate(input) {
+ return Console.isValid(input);
+ }
+
+ processLine() {
+ const {line} = this;
+ // Rebuild the input...
+ const js = [...this.#pendingInputLines, line].join('\n');
+
+ // Reset state...
+ this.#input = '';
+
+ // 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);
+ }
+ }
+
+ get inputPrompt() {
+ if (this.#pendingInputLines.length > 0) {
+ // Create a prefix like '... '
+ return ' '.padStart(4, '.');
+ }
+
+ return this.#prompt;
+ }
+
+ 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} options _
+ * @param {Gio.UnixOutputStream} options.stdin the input stream to treat as stdin
+ * @param {Gio.UnixOutputStream} options.stdout the output stream to treat as stdout
+ * @param {Gio.UnixOutputStream} options.stderr the output stream to treat as stderr
+ * @param {boolean} options.enableColor whether to print ANSI color codes
+ * @param {string} options.prompt the prompt to print prior to the line
+ */
+ 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();
+
+
+ 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.emit('exit');
+ } else {
+ this.#exitWarning = true;
+ this.print('\n(To exit, press Ctrl+C again or Ctrl+D)\n');
+ }
+ }
+
+ historyUp() {
+ if (this.historyIndex < this.#history.length - 1) {
+ this.historyIndex++;
+ this.cursor = -1;
+ }
+ }
+
+ historyDown() {
+ if (this.historyIndex >= 0) {
+ this.historyIndex--;
+ this.cursor = -1;
+ }
+ }
+
+ moveCursorToBeginning() {
+ this.cursor = 0;
+ }
+
+ moveCursorToEnd() {
+ this.cursor = this.line.length;
+ }
+
+ moveCursorLeft() {
+ this.cursor--;
+ }
+
+ moveCursorRight() {
+ this.cursor++;
+ }
+
+ addChar(char) {
+ this.line = StringSplice(this.line, this.cursor, 0, char);
+ this.moveCursorRight();
+ }
+
+ deleteChar() {
+ const {line} = this;
+
+ if (line.length > 0 && this.cursor > 0) {
+ const x = StringSplice(line, this.cursor - 1, 1);
+
+ this.line = x;
+ this.moveCursorLeft();
+ }
+ }
+
+ deleteCharRightOrClose() {
+ const {line} = this;
+
+ if (this.cursor < line.length - 1)
+ this.line = StringSplice(this.line, this.cursor, 1);
+ else
+ this.exit();
+ }
+
+ deleteToBeginning() {
+ this.line = StringSplice(this.line, 0, this.cursor);
+ }
+
+ deleteToEnd() {
+ this.line = StringSplice(this.line, this.cursor);
+ }
+
+ /**
+ * Adapted from lib/readline.js in Node.js
+ */
+ 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() {
+ 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() {
+ const {line} = this;
+ if (this.cursor > 0) {
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ const leading = line.slice(0, this.cursor);
+ const reversed = [...leading].reverse().join('');
+ const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+
+ this.cursor -= match[0].length;
+ this.cursor = Math.max(0, this.cursor);
+ }
+ }
+
+ /**
+ * Adapted from lib/readline.js in Node.js
+ */
+ wordRight() {
+ const {line} = this;
+
+ if (this.cursor < line.length) {
+ const trailing = line.slice(this.cursor);
+ const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
+
+ this.cursor += match[0].length;
+ }
+ }
+
+ processLine() {
+ const {line} = this;
+
+ this.#history.unshift(line);
+ this.historyIndex = -1;
+ this.#exitWarning = false;
+ this.cursor = 0;
+ this.#write('\n');
+
+ super.processLine();
+ }
+
+ #onKeyPress(sequence, key) {
+ this.#processKey(key);
+
+ if (!this.cancelled)
+ this.render();
+ }
+
+ #processKey(key) {
+ if (!key.sequence)
+ return;
+
+ if (key.ctrl && !key.meta && !key.shift) {
+ switch (key.name) {
+ case 'c':
+ this.exit();
+ return;
+ case 'h':
+ this.deleteChar();
+ return;
+ case 'd':
+ this.deleteCharRightOrClose();
+ return;
+ case 'u':
+ this.deleteToBeginning();
+ return;
+ case 'k':
+ this.deleteToEnd();
+ return;
+ case 'a':
+ this.moveCursorToBeginning();
+ return;
+ case 'e':
+ this.moveCursorToEnd();
+ return;
+ case 'b':
+ this.moveCursorLeft();
+ return;
+ case 'f':
+ this.moveCursorRight();
+ return;
+ case 'l':
+ Console.clearTerminal();
+ return;
+ case 'n':
+ this.historyDown();
+ return;
+ case 'p':
+ this.historyUp();
+ return;
+ case 'z':
+ // Pausing is unsupported.
+ return;
+ case 'w':
+ case 'backspace':
+ this.deleteWordLeft();
+ return;
+ case 'delete':
+ this.deleteWordRight();
+ return;
+ case 'left':
+ this.wordLeft();
+ return;
+ case 'right':
+ this.wordRight();
+ return;
+ }
+ } else if (key.meta && !key.shift) {
+ switch (key.name) {
+ case 'd':
+ this.deleteWordRight();
+ return;
+ case 'backspace':
+ this.deleteWordLeft();
+ return;
+ case 'b':
+ this.wordLeft();
+ return;
+ case 'f':
+ this.wordRight();
+ return;
+ }
+ }
+
+ switch (key.name) {
+ case 'up':
+ this.historyUp();
+ return;
+ case 'down':
+ this.historyDown();
+ return;
+ case 'left':
+ this.moveCursorLeft();
+ return;
+ case 'right':
+ this.moveCursorRight();
+ return;
+ case 'backspace':
+ this.deleteChar();
+ return;
+ case 'return':
+ this.processLine();
+ return;
+ }
+
+ this.addChar(key.sequence);
+ }
+
+ /**
+ * @param {number} column the column to move the cursor to
+ */
+ set cursor(column) {
+ if (column < 0) {
+ this.#cursorColumn = 0;
+ return;
+ }
+
+ // Ensure the input index isn't longer than the content...
+ this.#cursorColumn = Math.min(this.line.length, column);
+ }
+
+ get cursor() {
+ return this.#cursorColumn;
+ }
+
+ render() {
+ // Prevent the cursor from flashing while we render...
+ this.#write(cursorHide);
+
+ const {inputPrompt, line} = this;
+
+ this.#write(
+ cursorTo(0),
+ CSI.kClearScreenDown,
+ inputPrompt,
+ line,
+ cursorTo(inputPrompt.length + this.cursor),
+ cursorShow
+ );
+
+ this.emit('render');
+ }
+
+ #write(...strings) {
+ const bytes = new TextEncoder().encode(strings.join(''));
+
+ this.stdout.write_bytes(bytes, null);
+ this.stdout.flush(null);
+ }
+
+ /**
+ * @param {string[]} strings strings to write to stdout
+ */
+ print(...strings) {
+ this.#write(...strings, '\n');
+ }
+
+ /**
+ * @param {Uint8Array} bytes an array of inputted bytes to process
+ * @returns {void}
+ */
+ handleInput(bytes) {
+ if (bytes.length === 0)
+ return;
+
+ const input = String.fromCharCode(...bytes.values());
+
+ for (const byte of input) {
+ this.#parser.next(byte);
+
+ if (this.cancelled)
+ break;
+ }
+ }
+
+ #asyncReadHandler(stream, result) {
+ if (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);
+
+ return;
+ }
+ }
+ }
+
+ if (this.cancelled)
+ return;
+
+ this.#cancellable = new Gio.Cancellable();
+ stream.read_bytes_async(8, 0, this.#cancellable, this.#asyncReadHandler.bind(this));
+ }
+
+ cancel() {
+ super.cancel();
+
+ this.#cancellable?.cancel();
+ this.#cancellable = null;
+
+ this.#write('\n');
+ }
+
+ prompt() {
+ super.prompt();
+ this.render();
+
+ // Start the async read loop...
+ this.#asyncReadHandler(this.stdin);
+ }
+}
+
+export class SyncReadline extends Readline {
+ constructor({prompt}) {
+ super({prompt});
+ }
+
+ prompt() {
+ while (!this.cancelled) {
+ const {inputPrompt} = this;
+
+ try {
+ this.line = Console.interact(inputPrompt).split('');
+ } catch {
+ this.line = '';
+ }
+
+
+ this.processLine();
+ }
+ }
+
+ print(output) {
+ print(output);
+ }
+}
+
+export class Repl {
+ #lineNumber = 0;
+ #isAsync = false;
+
+ /** @type {boolean} */
+ #supportsColor;
+ /** @type {string} */
+ #version;
+
+ #mainloop = false;
+
+ constructor() {
+ ({Gio} = imports.gi);
+
+ this.#version = imports.system.versionString;
+
+ try {
+ this.#supportsColor = GLib.log_writer_supports_color(1) && GLib.getenv('NO_COLOR') === null;
+ } catch {
+ this.#supportsColor ||= false;
+ }
+
+ 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;
+ }
+ }
+
+ [Symbol.toStringTag]() {
+ return 'Repl';
+ }
+
+ get lineNumber() {
+ return this.#lineNumber;
+ }
+
+ get supportsColor() {
+ return this.#supportsColor;
+ }
+
+ #print(string) {
+ this.input.print(`${string}`);
+ }
+
+ #evaluateInternal(lines) {
+ try {
+ const result = Console.eval(lines, this.#lineNumber);
+
+ if (result !== undefined)
+ this.#print(`${toString(result)}`);
+
+ return null;
+ } catch (error) {
+ return error;
+ }
+ }
+
+ #printError(error) {
+ if (error.message)
+ this.#print(`Uncaught ${error.name}: ${error.message}`);
+ else
+ this.#print(`${toString(error)}`);
+ }
+
+ evaluate(lines) {
+ this.#lineNumber++;
+
+ // TODO(ewlsh): Object/code block detection similar to Node
+ let wrappedLines = lines.trim();
+ if (wrappedLines.startsWith('{') &&
+ !wrappedLines.endsWith(';'))
+ wrappedLines = `(${wrappedLines})\n`;
+
+ // Attempt to evaluate any object literals in () first
+ let error = this.#evaluateInternal(wrappedLines);
+ if (!error)
+ return;
+
+ error = this.#evaluateInternal(lines);
+ if (!error)
+ return;
+
+ this.#printError(error);
+ }
+
+ #start() {
+ this.input.print(`GJS v${this.#version}`);
+
+ this.input.connect('line', (_, line) => {
+ if (typeof line === 'string' && line.trim().startsWith('exit()'))
+ this.exit();
+ else
+ this.evaluate(line);
+ });
+
+ this.input.connect('exit', () => {
+ this.exit();
+ });
+
+ this.input.prompt();
+ }
+
+ start() {
+ if (!this.#isAsync) {
+ this.input = new SyncReadline({prompt: '> '});
+
+ this.#start();
+
+ return;
+ }
+
+ try {
+ const stdin = Gio.UnixInputStream.new(0, 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 AsyncReadline({
+ stdin,
+ stdout,
+ stderr,
+ enableColor: this.#supportsColor,
+ prompt: '> ',
+ });
+
+ this.#start();
+
+ this.#mainloop = true;
+ imports.mainloop.run('repl');
+ } finally {
+ Console.disableRawMode();
+ }
+ }
+
+ exit() {
+ try {
+ this.input.cancel();
+
+ 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);
+ }
+ }
+}
+
+imports.console.Repl = Repl;
diff --git a/modules/modules.cpp b/modules/modules.cpp
index c9b7061d4..f0a950d89 100644
--- a/modules/modules.cpp
+++ b/modules/modules.cpp
@@ -21,6 +21,7 @@ gjs_register_static_modules (void)
gjs_register_native_module("cairoNative", gjs_js_define_cairo_stuff);
#endif
gjs_register_native_module("system", gjs_js_define_system_stuff);
- gjs_register_native_module("console", gjs_define_console_stuff);
+ gjs_register_native_module("_consoleNative",
+ gjs_define_console_private_stuff);
gjs_register_native_module("_print", gjs_define_print_stuff);
}
diff --git a/modules/script/console.js b/modules/script/console.js
new file mode 100644
index 000000000..d80b699c1
--- /dev/null
+++ b/modules/script/console.js
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+/* exported Repl, interact */
+
+var Repl = null;
+
+function interact() {
+ const repl = new Repl();
+
+ repl.start();
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]