summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvan Welsh <contact@evanwelsh.com>2023-03-12 13:08:08 -0700
committerEvan Welsh <contact@evanwelsh.com>2023-03-12 13:08:08 -0700
commit96c80122e474aac114d0d52acde1f2de3a711b85 (patch)
tree28e834f09788922be2adebf7b89a913fc0dc60d4
parent064894bf82755f51aaa1f762c06cacc8bf55720f (diff)
downloadgjs-ewlsh/nova-repl.tar.gz
feat: Console async workewlsh/nova-repl
-rw-r--r--gjs/console.cpp19
-rw-r--r--installed-tests/js/.eslintrc.yml1
-rw-r--r--installed-tests/js/meson.build1
-rw-r--r--installed-tests/js/testReadline.js161
-rw-r--r--js.gresource.xml11
-rw-r--r--meson.build1
-rw-r--r--modules/console.cpp334
-rw-r--r--modules/console.h4
-rw-r--r--modules/esm/_bootstrap/default.js6
-rw-r--r--modules/esm/_bootstrap/repl.js10
-rw-r--r--modules/esm/_legacyGlobal.js53
-rw-r--r--modules/esm/_prettyPrint.js118
-rw-r--r--modules/esm/_readline/callbacks.js72
-rw-r--r--modules/esm/_readline/primordials.js20
-rw-r--r--modules/esm/_readline/terminal.js220
-rw-r--r--modules/esm/_readline/utils.js397
-rw-r--r--modules/esm/console.js25
-rw-r--r--modules/esm/readline.js622
-rw-r--r--modules/esm/repl.js150
-rw-r--r--modules/internal/loader.js31
-rw-r--r--modules/modules.cpp2
-rw-r--r--modules/script/_bootstrap/default.js166
-rw-r--r--modules/script/console.js12
23 files changed, 2041 insertions, 395 deletions
diff --git a/gjs/console.cpp b/gjs/console.cpp
index c2e15789..02917b42 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -30,6 +30,7 @@ static GjsAutoChar command;
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;
@@ -48,6 +49,7 @@ static GOptionEntry entries[] = {
"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.out(),
"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.out(), "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,
@@ -274,6 +276,7 @@ int 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)) {
@@ -308,9 +311,19 @@ int main(int argc, char** argv) {
return EXIT_FAILURE;
}
- 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 74a8a4f3..21a6eada 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -34,6 +34,7 @@ overrides:
- testESModules.js
- testEncoding.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 6db887d2..35f9e0e4 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -240,6 +240,7 @@ endforeach
modules_tests = [
'Async',
'Console',
+ 'Readline',
'ESModules',
'AsyncMainloop',
'Encoding',
diff --git a/installed-tests/js/testReadline.js b/installed-tests/js/testReadline.js
new file mode 100644
index 00000000..02e04b8e
--- /dev/null
+++ b/installed-tests/js/testReadline.js
@@ -0,0 +1,161 @@
+// 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 'readline';
+
+function createReadline() {
+ return new AsyncReadline({
+ inputStream: null,
+ outputStream: null,
+ errorOutputStream: null,
+ prompt: '> ',
+ enableColor: false,
+ });
+}
+
+function createReadlineWithStreams() {
+ const inputStream = new Gio.MemoryInputStream();
+ const outputStream = Gio.MemoryOutputStream.new_resizable();
+ const errorOutputStream = Gio.MemoryOutputStream.new_resizable();
+
+ const readline = new AsyncReadline({
+ inputStream,
+ outputStream,
+ errorOutputStream,
+ prompt: '> ',
+ enableColor: false,
+ });
+
+ return {
+ readline,
+ inputStream,
+ teardown() {
+ readline.cancel();
+
+ try {
+ readline.stdout.close(null);
+ } catch {}
+
+ try {
+ readline.stdin.close(null);
+ } catch {}
+
+ try {
+ readline.stderr.close(null);
+ } catch {}
+ },
+ };
+}
+
+function expectReadlineOutput({
+ readline,
+ inputStream,
+ 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();
+ }
+ });
+
+ inputStream.add_bytes(new TextEncoder().encode(input));
+ });
+}
+
+
+
+describe('Readline', () => {
+ describe('AsyncReadline', () => {
+ it('handles key events on stdin', async function () {
+ const { readline, inputStream, teardown } = createReadlineWithStreams();
+
+ readline.prompt();
+
+ await expectReadlineOutput({
+ readline,
+ inputStream,
+ input: 'a',
+ output: 'a',
+ });
+
+ await expectReadlineOutput({
+ readline,
+ inputStream,
+ input: 'b',
+ output: 'ab',
+ });
+
+ await expectReadlineOutput({
+ readline,
+ inputStream,
+ input: '\x1b[D\x1b[Dcr',
+ output: 'crab',
+ keystrokes: 4,
+ });
+
+ teardown();
+ });
+ });
+
+ 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 e12dea12..25a2e178 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -8,6 +8,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>
@@ -15,10 +16,19 @@
<file>modules/esm/_timers.js</file>
+ <file>modules/esm/_readline/utils.js</file>
+ <file>modules/esm/_readline/primordials.js</file>
+ <file>modules/esm/_readline/callbacks.js</file>
+ <file>modules/esm/_readline/terminal.js</file>
+
<file>modules/esm/cairo.js</file>
+ <file>modules/esm/_prettyPrint.js</file>
+ <file>modules/esm/_legacyGlobal.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/readline.js</file>
<file>modules/esm/system.js</file>
<!-- Script-based Modules -->
@@ -32,6 +42,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/meson.build b/meson.build
index 4410a110..0934784a 100644
--- a/meson.build
+++ b/meson.build
@@ -337,6 +337,7 @@ if build_readline
endif
header_conf.set('USE_UNITY_BUILD', get_option('unity'))
header_conf.set('HAVE_SYS_SYSCALL_H', cxx.check_header('sys/syscall.h'))
+header_conf.set('HAVE_SYS_IOCTL_H', cxx.check_header('sys/ioctl.h'))
header_conf.set('HAVE_TERMIOS_H', cxx.check_header('termios.h'))
header_conf.set('HAVE_UNISTD_H', cxx.check_header('unistd.h'))
header_conf.set('HAVE_SIGNAL_H', cxx.check_header('signal.h',
diff --git a/modules/console.cpp b/modules/console.cpp
index 35df0d2a..f2f6c140 100644
--- a/modules/console.cpp
+++ b/modules/console.cpp
@@ -3,29 +3,24 @@
// 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.h>
#include <glib/gprintf.h> // for g_fprintf
+#include <stdio.h>
+
+#if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_UNISTD_H)
+# include <fcntl.h>
+# include <sys/ioctl.h>
+# include <unistd.h>
+# if defined(TIOCGWINSZ)
+# define GET_SIZE_USE_IOCTL
+# endif
+#endif
#include <js/CallAndConstruct.h>
#include <js/CallArgs.h>
@@ -35,6 +30,8 @@
#include <js/ErrorReport.h>
#include <js/Exception.h>
#include <js/PropertyAndElement.h>
+#include <js/PropertyDescriptor.h>
+#include <js/PropertySpec.h>
#include <js/RootingAPI.h>
#include <js/SourceText.h>
#include <js/TypeDecls.h>
@@ -47,18 +44,16 @@
#include "gjs/atoms.h"
#include "gjs/context-private.h"
#include "gjs/global.h"
+#include "gjs/jsapi-util-args.h"
#include "gjs/jsapi-util.h"
#include "gjs/macros.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;
@@ -98,84 +93,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;
}
-std::string print_string_value(JSContext* cx, JS::HandleValue v_string) {
- if (!v_string.isString())
- return "[unexpected result from printing value]";
-
- JS::RootedString printed_string(cx, v_string.toString());
- JS::AutoSaveExceptionState exc_state(cx);
- JS::UniqueChars chars(JS_EncodeStringToUTF8(cx, printed_string));
- exc_state.restore();
- if (!chars)
- return "[error printing value]";
-
- return chars.get();
-}
-
-/* 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,
- JS::HandleObject global,
- 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))
@@ -184,132 +115,86 @@ std::string print_string_value(JSContext* cx, JS::HandleValue v_string) {
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())
- return true;
+ 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::AutoSaveExceptionState exc_state(cx);
- JS::RootedValue v_printed_string(cx);
- JS::RootedValue v_pretty_print(
- cx, gjs_get_global_slot(global, GjsGlobalSlot::PRETTY_PRINT_FUNC));
- bool ok = JS::Call(cx, global, v_pretty_print, JS::HandleValueArray(result),
- &v_printed_string);
- if (!ok)
- gjs_log_exception(cx);
- exc_state.restore();
-
- if (ok) {
- g_fprintf(stdout, "%s\n",
- print_string_value(cx, v_printed_string).c_str());
- } else {
- g_fprintf(stdout, "[error printing value]\n");
+ 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;
}
+ 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);
- volatile bool eof, exit_warning; // accessed after setjmp()
- JS::RootedObject global(context, gjs_get_import_global(context));
- char* temp_buf;
- volatile int lineno; // accessed after setjmp()
- volatile int startline; // accessed after setjmp()
+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;
-#ifndef HAVE_READLINE_READLINE_H
- int rl_end = 0; // nonzero if using readline and any text is typed in
-#endif
+ args.rval().setBoolean(Gjs::Console::disable_raw_mode());
+ return true;
+}
- 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, global, 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);
+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;
+ 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;
+
+ JS::UniqueChars code;
+ size_t code_len;
+ if (!gjs_string_to_utf8_n(cx, str, &code, &code_len))
+ return false;
- argv.rval().setUndefined();
+ JS::RootedObject global(cx, gjs_get_import_global(cx));
+
+ args.rval().setBoolean(
+ JS_Utf8BufferIsCompilableUnit(cx, global, code.get(), code_len));
return true;
}
@@ -329,10 +214,57 @@ static bool gjs_console_clear_terminal(JSContext* cx, unsigned argc,
return true;
}
+bool gjs_console_get_terminal_size(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+ if (!obj)
+ return false;
+
+ // Use 'int' because Windows uses int values, whereas most Unix systems
+ // use 'short'
+ unsigned int width, height;
+
+ JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+#ifdef GET_SIZE_USE_IOCTL
+ struct winsize ws;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
+ gjs_throw(cx, "No terminal output is present.\n");
+ return false;
+ }
+
+ width = ws.ws_col;
+ height = ws.ws_row;
+#else
+ // TODO(ewlsh): Implement Windows equivalent.
+ // See
+ // https://docs.microsoft.com/en-us/windows/console/window-and-screen-buffer-size.
+ gjs_throw(cx, "Unable to retrieve terminal size on this platform.\n");
+ return false;
+#endif
+
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+ if (!JS_DefinePropertyById(cx, obj, atoms.height(), height,
+ JSPROP_READONLY) ||
+ !JS_DefinePropertyById(cx, obj, atoms.width(), width, JSPROP_READONLY))
+ return false;
+
+ argv.rval().setObject(*obj);
+ return true;
+}
+
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("getDimensions", gjs_console_get_terminal_size, 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 73ed4c0e..7df6a02d 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 ff1f28bf..1d9b6465 100644
--- a/modules/esm/_bootstrap/default.js
+++ b/modules/esm/_bootstrap/default.js
@@ -9,3 +9,9 @@ import '_encoding/encoding';
import 'console';
// Bootstrap the Timers API
import '_timers';
+// Install the Repl constructor for Console.interact()
+import 'repl';
+// Install the pretty printing global function
+import '_prettyPrint';
+// Setup legacy global values
+import '_legacyGlobal';
diff --git a/modules/esm/_bootstrap/repl.js b/modules/esm/_bootstrap/repl.js
new file mode 100644
index 00000000..cb8e98eb
--- /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/_legacyGlobal.js b/modules/esm/_legacyGlobal.js
new file mode 100644
index 00000000..c6d4bd2f
--- /dev/null
+++ b/modules/esm/_legacyGlobal.js
@@ -0,0 +1,53 @@
+import { prettyPrint } from './_prettyPrint.js';
+
+const {
+ print,
+ printerr,
+ log: nativeLog,
+ logError: nativeLogError,
+} = import.meta.importSync('_print');
+
+function log(...args) {
+ return nativeLog(args.map(arg => typeof arg === 'string' ? arg : prettyPrint(arg)).join(' '));
+}
+
+function logError(e, ...args) {
+ if (args.length === 0)
+ return nativeLogError(e);
+ return nativeLogError(e, args.map(arg => typeof arg === 'string' ? arg : prettyPrint(arg)).join(' '));
+}
+
+Object.defineProperties(globalThis, {
+ ARGV: {
+ configurable: false,
+ enumerable: true,
+ get() {
+ // Wait until after bootstrap or programArgs won't be set.
+ return imports.system.programArgs;
+ },
+ },
+ print: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: print,
+ },
+ printerr: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: printerr,
+ },
+ log: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: log,
+ },
+ logError: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: logError,
+ },
+});
diff --git a/modules/esm/_prettyPrint.js b/modules/esm/_prettyPrint.js
new file mode 100644
index 00000000..f379ba9c
--- /dev/null
+++ b/modules/esm/_prettyPrint.js
@@ -0,0 +1,118 @@
+const { setPrettyPrintFunction } = import.meta.importSync('_print');
+
+/**
+ * @param {unknown} value
+ * @returns
+ */
+export function prettyPrint(value) {
+ if (value === null) return 'null';
+ if (value === undefined) return 'undefined';
+
+ switch (typeof value) {
+ case 'object':
+ if (value.toString === Object.prototype.toString ||
+ value.toString === Array.prototype.toString ||
+ value.toString === Date.prototype.toString) {
+ const printedObjects = new WeakSet();
+ return formatObject(value, printedObjects);
+ }
+ // If the object has a nonstandard toString, prefer that
+ return value.toString();
+ case 'function':
+ if (value.toString === Function.prototype.toString)
+ return formatFunction(value);
+ return value.toString();
+ case 'string':
+ return JSON.stringify(value);
+ case 'symbol':
+ return formatSymbol(value);
+ default:
+ return value.toString();
+ }
+}
+
+function formatPropertyKey(key) {
+ if (typeof key === 'symbol')
+ return `[${formatSymbol(key)}]`;
+ return `${key}`;
+}
+
+function formatObject(obj, printedObjects) {
+ printedObjects.add(obj);
+ if (Array.isArray(obj))
+ return formatArray(obj, printedObjects).toString();
+
+ if (obj instanceof Date)
+ return formatDate(obj);
+
+ if (obj[Symbol.toStringTag] === 'GIRepositoryNamespace')
+ return obj.toString();
+
+ const formattedObject = [];
+ const keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
+ for (const propertyKey of keys) {
+ const value = obj[propertyKey];
+ const key = formatPropertyKey(propertyKey);
+ switch (typeof value) {
+ case 'object':
+ if (printedObjects.has(value))
+ formattedObject.push(`${key}: [Circular]`);
+ else
+ formattedObject.push(`${key}: ${formatObject(value, printedObjects)}`);
+ break;
+ case 'function':
+ formattedObject.push(`${key}: ${formatFunction(value)}`);
+ break;
+ case 'string':
+ formattedObject.push(`${key}: "${value}"`);
+ break;
+ case 'symbol':
+ formattedObject.push(`${key}: ${formatSymbol(value)}`);
+ break;
+ default:
+ formattedObject.push(`${key}: ${value}`);
+ break;
+ }
+ }
+ return Object.keys(formattedObject).length === 0 ? '{}'
+ : `{ ${formattedObject.join(', ')} }`;
+}
+
+function formatArray(arr, printedObjects) {
+ const formattedArray = [];
+ for (const [key, value] of arr.entries()) {
+ if (printedObjects.has(value))
+ formattedArray[key] = '[Circular]';
+ else
+ formattedArray[key] = prettyPrint(value);
+ }
+ return `[${formattedArray.join(', ')}]`;
+}
+
+function formatDate(date) {
+ return date.toISOString();
+}
+
+function formatFunction(func) {
+ let funcOutput = `[ Function: ${func.name} ]`;
+ return funcOutput;
+}
+
+function formatSymbol(sym) {
+ // Try to format Symbols in the same way that they would be constructed.
+
+ // First check if this is a global registered symbol
+ const globalKey = Symbol.keyFor(sym);
+ if (globalKey !== undefined)
+ return `Symbol.for("${globalKey}")`;
+
+ const descr = sym.description;
+ // Special-case the 'well-known' (built-in) Symbols
+ if (descr.startsWith('Symbol.'))
+ return descr;
+
+ // Otherwise, it's just a regular symbol
+ return `Symbol("${descr}")`;
+}
+
+setPrettyPrintFunction(globalThis, prettyPrint);
diff --git a/modules/esm/_readline/callbacks.js b/modules/esm/_readline/callbacks.js
new file mode 100644
index 00000000..109543f8
--- /dev/null
+++ b/modules/esm/_readline/callbacks.js
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: Node.js contributors. All rights reserved.
+
+import {CSI} from './utils.js';
+
+const {Number} = globalThis;
+
+const NumberIsNaN = Number.isNaN;
+
+// Adapted from https://github.com/nodejs/node/blob/1b550ba1af50a9e7eed9b27a92902115f98cf4d8/lib/internal/readline/callbacks.js
+
+/* eslint-disable */
+
+const {
+ kClearLine,
+ kClearToLineBeginning,
+ kClearToLineEnd,
+} = CSI;
+
+/**
+ * moves the cursor to the x and y coordinate on the given stream
+ */
+
+function cursorTo(x, y) {
+ if (NumberIsNaN(x)) throw new Error('Invalid argument x is NaN');
+ if (NumberIsNaN(y)) throw new Error('Invalid argument y is NaN');
+ if (typeof x !== 'number') throw new Error('Invalid argument x is not a number');
+
+ 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
+ */
+
+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
+ */
+
+function clearLine(dir) {
+ const type =
+ dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
+ return type;
+}
+
+export {
+ clearLine,
+ cursorTo,
+ moveCursor,
+};
diff --git a/modules/esm/_readline/primordials.js b/modules/esm/_readline/primordials.js
new file mode 100644
index 00000000..57638904
--- /dev/null
+++ b/modules/esm/_readline/primordials.js
@@ -0,0 +1,20 @@
+// 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]>}
+ */
+export function uncurryThis(type, key) {
+ const func = type[key];
+ return (instance, ...args) => func.apply(instance, args);
+}
diff --git a/modules/esm/_readline/terminal.js b/modules/esm/_readline/terminal.js
new file mode 100644
index 00000000..7610c9e6
--- /dev/null
+++ b/modules/esm/_readline/terminal.js
@@ -0,0 +1,220 @@
+
+import GLib from 'gi://GLib';
+import gi from 'gi';
+
+import { emitKeys, CSI } from './utils.js';
+import { cursorTo } from './callbacks.js';
+
+const NativeConsole = import.meta.importSync('_consoleNative');
+
+/** @type {import('gi://Gio')} */
+let Gio;
+
+function requireGio() {
+ if (!Gio) Gio = gi.require('Gio');
+}
+
+const cursorHide = CSI`?25l`;
+const cursorShow = CSI`?25h`;
+
+/**
+ * @typedef {object} TerminalOptions
+* @property {Gio.InputStream | null} [inputStream] the input stream
+* @property {Gio.OutputStream | null} [outputStream] the output stream
+* @property {Gio.OutputStream | null} [errorOutputStream] the error output stream
+* @property {boolean} [enableColor] whether to print ANSI color codes
+ */
+
+export class Terminal {
+ /**
+ * Pending writes to the stream
+ */
+ #buffer = [];
+ /**
+ * @type {Gio.Cancellable | null}
+ */
+ #cancellable = null;
+
+ #parser;
+
+ /**
+ * @param {TerminalOptions} options _
+ */
+ constructor({
+ onKeyPress,
+ inputStream = Terminal.stdin,
+ outputStream = Terminal.stdout,
+ errorOutputStream = Terminal.stderr,
+ enableColor,
+ }) {
+ this.inputStream = inputStream;
+ this.outputStream = outputStream;
+ this.errorOutputStream = errorOutputStream;
+ this.enableColor = enableColor ?? this.supportsColor();
+ this.cancelled = false;
+
+ this.#parser = emitKeys(onKeyPress);
+ this.#parser.next();
+ }
+
+ get dimensions() {
+ const values = NativeConsole.getDimensions();
+ return { height: values.height, width: values.width };
+ }
+
+ commit() {
+ const bytes = new TextEncoder().encode(this.#buffer.join(''));
+ this.#buffer = [];
+
+ if (!this.outputStream) return;
+
+ this.outputStream.write_bytes(bytes, null);
+ this.outputStream.flush(null);
+ }
+
+ print(...strings) {
+ this.#buffer.push(...strings);
+ }
+
+ clearScreenDown() {
+ this.#buffer.push(CSI.kClearScreenDown);
+ }
+
+ newLine() {
+ this.#buffer.push('\n');
+ }
+ hideCursor() {
+ this.#buffer.push(cursorHide);
+ }
+
+ showCursor() {
+ this.#buffer.push(cursorShow);
+ }
+
+ cursorTo(x, y) {
+ this.#buffer.push(cursorTo(x, y));
+ }
+ /**
+ * @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) {
+ requireGio();
+
+ 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)
+ );
+ }
+
+ stopInput() {
+ this.cancelled = true;
+
+ this.#cancellable?.cancel();
+ this.#cancellable = null;
+
+ this.#buffer = [];
+ }
+
+ startInput() {
+ if (!this.inputStream) throw new Error('Terminal has no input stream')
+
+ this.#asyncReadHandler(this.inputStream);
+ }
+
+ supportsColor() {
+ return (
+ this.outputStream &&
+ GLib.log_writer_supports_color(this.outputStream.fd) &&
+ GLib.getenv('NO_COLOR') === null
+ );
+ }
+
+ static hasUnixStreams() {
+ requireGio();
+
+ return 'UnixInputStream' in Gio && 'UnixOutputStream' in Gio;
+ }
+
+ static #stdin = null;
+ static #stdout = null;
+ static #stderr = null;
+
+ static get stdout() {
+ if (!Terminal.hasUnixStreams()) {
+ throw new Error('Missing Gio.UnixOutputStream');
+ }
+
+ requireGio();
+
+ return (this.#stdout ??= new Gio.BufferedOutputStream({
+ baseStream: Gio.UnixOutputStream.new(1, false),
+ closeBaseStream: false,
+ autoGrow: true,
+ }));
+ }
+
+ static get stdin() {
+ if (!Terminal.hasUnixStreams()) {
+ throw new Error('Missing Gio.UnixInputStream');
+ }
+
+ requireGio();
+
+ return (this.#stdin ??= Gio.UnixInputStream.new(0, false));
+ }
+
+ static get stderr() {
+ if (!Terminal.hasUnixStreams()) {
+ throw new Error('Missing Gio.UnixOutputStream');
+ }
+
+ requireGio();
+
+ return (this.#stderr ??= Gio.UnixOutputStream.new(2, false));
+ }
+}
+
+export function setRawMode(enabled) {
+ if (enabled) {
+ const success = NativeConsole.enableRawMode();
+
+ if (!success) throw new Error('Could not set raw mode on stdin');
+ } else {
+ NativeConsole.disableRawMode();
+ }
+}
diff --git a/modules/esm/_readline/utils.js b/modules/esm/_readline/utils.js
new file mode 100644
index 00000000..b03c1c8e
--- /dev/null
+++ b/modules/esm/_readline/utils.js
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: Node.js contributors. All rights reserved.
+
+import {uncurryThis} from './primordials.js';
+
+const ArrayPrototypeSlice = uncurryThis(Array.prototype, 'slice');
+const ArrayPrototypeSort = uncurryThis(Array.prototype, 'sort');
+const RegExpPrototypeExec = uncurryThis(RegExp.prototype, 'exec');
+const StringFromCharCode = String.fromCharCode;
+const StringPrototypeCharCodeAt = uncurryThis(String.prototype, 'charCodeAt');
+const StringPrototypeCodePointAt = uncurryThis(String.prototype, 'codePointAt');
+const StringPrototypeSlice = uncurryThis(String.prototype, 'slice');
+const StringPrototypeToLowerCase = uncurryThis(String.prototype, 'toLowerCase');
+
+
+// Adapted from https://github.com/nodejs/node/blob/56679eb53044b03e4da0f7420774d54f0c550eec/lib/internal/readline/utils.js
+
+/* eslint-disable */
+
+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
+*/
+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 = RegExpPrototypeExec(/^(\d\d?)(;(\d))?([~^$])$/, cmd))) {
+ code += match[1] + match[4];
+ modifier = (match[3] || 1) - 1;
+ } else if (
+ (match = RegExpPrototypeExec(/^((\d;)?(\d))?([A-Za-z])$/, cmd))
+ ) {
+ 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 (RegExpPrototypeExec(/^[0-9A-Za-z]$/, ch) !== null) {
+ // Letter, number, shift+letter
+ key.name = StringPrototypeToLowerCase(ch);
+ key.shift = RegExpPrototypeExec(/^[A-Z]$/, ch) !== null;
+ 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).
+function commonPrefix(strings) {
+ if (strings.length === 0) {
+ return '';
+ }
+ 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/console.js b/modules/esm/console.js
index 9b57f97b..0bc7deb2 100644
--- a/modules/esm/console.js
+++ b/modules/esm/console.js
@@ -3,6 +3,8 @@
import GLib from 'gi://GLib';
+import { prettyPrint } from './_prettyPrint.js';
+
const NativeConsole = import.meta.importSync('_consoleNative');
const DEFAULT_LOG_DOMAIN = 'Gjs-Console';
@@ -36,7 +38,15 @@ function hasFormatSpecifiers(str) {
* @param {any} item an item to format
*/
function formatGenerically(item) {
- return JSON.stringify(item, null, 4);
+ if (typeof item === 'string') {
+ return JSON.stringify(item, null, 4);
+ }
+
+ try {
+ return JSON.stringify(item, null, 4);
+ } catch {
+ return `${item}`;
+ }
}
/**
@@ -44,6 +54,7 @@ function formatGenerically(item) {
* @returns {string}
*/
function formatOptimally(item) {
+ try {
// Handle optimal error formatting.
if (item instanceof Error) {
return `${item.toString()}${item.stack ? '\n' : ''}${item.stack
@@ -53,10 +64,12 @@ function formatOptimally(item) {
.join('\n')}`;
}
- // TODO: Enhance 'optimal' formatting.
- // There is a current work on a better object formatter for GJS in
- // https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/587
- return JSON.stringify(item, null, 4);
+
+ return prettyPrint(item);
+} catch {
+}
+
+return formatGenerically(item);
}
/**
@@ -490,7 +503,7 @@ class Console {
* https://console.spec.whatwg.org/#printer
*
* This implementation of Printer maps WHATWG log severity to
- * {@see GLib.LogLevelFlags} and outputs using GLib structured logging.
+ * {@link GLib.LogLevelFlags} and outputs using GLib structured logging.
*
* @param {string} logLevel the log level (log tag) the args should be
* emitted with
diff --git a/modules/esm/readline.js b/modules/esm/readline.js
new file mode 100644
index 00000000..30e07a7c
--- /dev/null
+++ b/modules/esm/readline.js
@@ -0,0 +1,622 @@
+import GLib from 'gi://GLib';
+import gi from 'gi';
+
+import { charLengthAt, charLengthLeft } from './_readline/utils.js';
+import {Terminal} from './_readline/terminal.js';
+
+export {Terminal, setRawMode} from './_readline/terminal.js';
+
+
+const NativeConsole = import.meta.importSync('_consoleNative');
+
+/**
+ * @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)
+ );
+}
+
+/**
+ * @typedef {object} ReadlineOptions
+ * @property {string} prompt the prompt to print prior to the line
+ * @property {string} [pendingPrompt] the prompt shown when multiline input is pending
+ * @property {boolean} [enableColor] whether to print ANSI color codes
+ */
+
+/** @typedef {ReadlineOptions & import('./_readline/terminal.js').TerminalOptions} AsyncReadlineOptions */
+
+/**
+ * A basic abstraction around line-by-line input
+ */
+export class Readline {
+ #prompt;
+ #pendingPrompt;
+ /**
+ * The current input
+ */
+ #input = '';
+ /**
+ * Whether to cancel the prompt
+ */
+ #cancelling = false;
+
+ /**
+ * Store pending lines of multiline input
+ *
+ * @example
+ * gjs > 'a pending line...
+ * ..... '
+ *
+ * @type {string[]}
+ */
+ #pendingInputLines = [];
+
+ get isRaw() {
+ throw new Error('Unimplemented');
+ }
+
+ get isAsync() {
+ throw new Error('Unimplemented');
+ }
+
+ /**
+ * @param {ReadlineOptions} options _
+ */
+ constructor({ prompt, pendingPrompt = ' '.padStart(4, '.') }) {
+ this.#prompt = prompt;
+ this.#pendingPrompt = pendingPrompt;
+ }
+
+ [Symbol.toStringTag]() {
+ return 'Readline';
+ }
+
+ get cancelled() {
+ return this.#cancelling;
+ }
+
+ get line() {
+ return this.#input;
+ }
+
+ set line(value) {
+ this.#input = value;
+ }
+
+ hasMultilineInput() {
+ return this.#pendingInputLines.length > 0;
+ }
+
+ addLineToMultilineInput(line) {
+ // Buffer the pending input...
+ this.#pendingInputLines.push(line);
+ }
+
+ completeMultilineInput() {
+ // Reset lines before input is triggered
+ this.#pendingInputLines = [];
+ }
+
+ processLine() {
+ const { line } = this;
+
+ // Rebuild the input...
+ const multilineInput = [...this.#pendingInputLines, line].join('\n');
+
+ this.emit('validate', line, multilineInput);
+
+ // Reset state...
+ this.#input = '';
+ if (this.#pendingInputLines.length > 0) {
+ return;
+ }
+
+ if (multilineInput.includes('\n')) {
+ this.emit('multiline', multilineInput);
+ } else {
+ this.emit('line', multilineInput);
+ }
+ }
+
+ get inputPrompt() {
+ if (this.#pendingInputLines.length > 0) {
+ return this.#pendingPrompt;
+ }
+
+ return this.#prompt;
+ }
+
+ print(_output) {}
+
+ render() {}
+
+ prompt() {
+ this.#cancelling = false;
+ }
+
+ exit() {}
+
+ cancel() {
+ this.#cancelling = true;
+ }
+
+ /**
+ * @param {ReadlineOptions} options _
+ */
+ static create(options) {
+ let isAsync = false;
+
+ try {
+ // Only enable async input if GJS_READLINE_USE_FALLBACK is not 'true'
+ isAsync = GLib.getenv('GJS_READLINE_USE_FALLBACK') !== 'true';
+ // Only enable async input if the terminal supports Unix streams
+ isAsync &&= Terminal.hasUnixStreams();
+ } catch {
+ // Otherwise, disable async
+ isAsync = false;
+ }
+
+ if (isAsync) {
+ return new AsyncReadline(options);
+ }
+
+ return new SyncReadline(options);
+ }
+}
+imports.signals.addSignalMethods(Readline.prototype);
+
+/**
+ * Synchronously reads lines and emits events to handle them
+ */
+export class SyncReadline extends Readline {
+ /**
+ * @param {ReadlineOptions} options _
+ */
+ constructor({ prompt, pendingPrompt }) {
+ super({ prompt, pendingPrompt, enableColor: false });
+ }
+
+ prompt() {
+ while (!this.cancelled) {
+ const { inputPrompt } = this;
+
+ try {
+ this.line = NativeConsole.interact(inputPrompt).split('');
+ } catch {
+ this.line = '';
+ }
+
+ this.processLine();
+ }
+ }
+
+ print(output) {
+ print(output);
+ }
+
+ get isAsync() {
+ return false;
+ }
+
+ get isRaw() {
+ return false;
+ }
+}
+
+/**
+ * Asynchronously reads lines and prints output, allowing a mainloop
+ * to run in parallel.
+ */
+export class AsyncReadline extends Readline {
+ #exitWarning = false;
+
+ /**
+ * Store previously inputted lines
+ *
+ * @type {string[]}
+ */
+ #history = [];
+ #historyIndex = -1;
+
+ /**
+ * The cursor's current column position.
+ */
+ #cursorColumn = 0;
+
+ /**
+ * @type {Terminal}
+ */
+ #terminal;
+
+ /**
+ * @param {AsyncReadlineOptions} options _
+ */
+ constructor({
+ inputStream,
+ outputStream,
+ errorOutputStream,
+ prompt,
+ pendingPrompt
+ }) {
+ super({ prompt, pendingPrompt });
+
+ this.#terminal = new Terminal({
+ inputStream,
+ outputStream,
+ errorOutputStream,
+ onKeyPress: (_, key) => {
+ this.#processKey(key);
+
+ if (!this.cancelled) this.render();
+ },
+ });
+ }
+
+ get isAsync() {
+ return true;
+ }
+
+ get isRaw() {
+ return true;
+ }
+
+ /**
+ * Gets the current line of input or a line from history if the user has scrolled up
+ */
+ get line() {
+ if (this.#historyIndex > -1) return this.#history[this.#historyIndex];
+
+ return super.line;
+ }
+
+ /**
+ * Modifies the current line of input or a line from history if the user has scrolled up
+ */
+ 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.moveCursorToEnd();
+ }
+ }
+
+ historyDown() {
+ if (this.#historyIndex >= 0) {
+ this.#historyIndex--;
+ this.moveCursorToEnd();
+ }
+ }
+
+ moveCursorToBeginning() {
+ this.cursor = 0;
+ }
+
+ moveCursorToEnd() {
+ this.cursor = this.line.length;
+ }
+
+ moveCursorLeft() {
+ this.cursor -= charLengthLeft(this.line, this.cursor);
+ }
+
+ moveCursorRight() {
+ this.cursor += charLengthAt(this.line, this.cursor);
+ }
+
+ addChar(char) {
+ this.line = StringSplice(this.line, this.cursor, 0, char);
+ this.moveCursorRight();
+ }
+
+ deleteChar() {
+ const { line, cursor } = this;
+
+ if (line.length > 0 && cursor > 0) {
+ const charLength = charLengthLeft(this.line, this.cursor);
+ const modified = StringSplice(
+ line,
+ cursor - charLength,
+ charLength
+ );
+
+ this.line = modified;
+ this.moveCursorLeft();
+ }
+ }
+
+ deleteCharRightOrClose() {
+ const { line, cursor } = this;
+
+ if (cursor < line.length - 1) {
+ const charLength = charLengthAt(this.line, this.cursor);
+ this.line = StringSplice(line, cursor, charLength);
+ return;
+ }
+
+ this.exit();
+ }
+
+ deleteToBeginning() {
+ this.line = StringSplice(this.line, 0, this.cursor);
+ }
+
+ deleteToEnd() {
+ this.line = StringSplice(this.line, this.cursor);
+ }
+
+ /**
+ * Adapted from lib/internal/readline/interface.js in Node.js
+ */
+ deleteWordLeft() {
+ const { line, cursor } = this;
+
+ if (cursor > 0) {
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ let leading = line.slice(0, 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(cursor));
+ this.cursor = leading.length;
+ }
+ }
+
+ /**
+ * Adapted from lib/internal/readline/interface.js in Node.js
+ */
+ deleteWordRight() {
+ const { line, cursor } = this;
+
+ if (line.length > 0 && cursor < line.length) {
+ const trailing = line.slice(cursor);
+ const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
+ this.line = line
+ .slice(0, cursor)
+ .concat(trailing.slice(match[0].length));
+ }
+ }
+
+ /**
+ * Adapted from lib/internal/readline/interface.js in Node.js
+ */
+ wordLeft() {
+ const { line, cursor } = this;
+
+ if (cursor > 0) {
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ const leading = line.slice(0, cursor);
+ const reversed = [...leading].reverse().join('');
+ const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+
+ this.cursor -= match[0].length;
+ }
+ }
+
+ /**
+ * Adapted from lib/internal/readline/interface.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;
+ }
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * The current column the cursor is at
+ */
+ get cursor() {
+ return this.#cursorColumn;
+ }
+
+ processLine() {
+ const { line } = this;
+
+ // Add the line to history
+ this.#history.unshift(line);
+
+ // Reset the CTRL-C exit warning
+ this.#exitWarning = false;
+
+ // Move the cursor to the beginning of the new line
+ this.moveCursorToBeginning();
+
+ // Print a newline
+ this.#terminal.newLine();
+ // Commit changes
+ this.#terminal.commit();
+
+ // Call Readline.processLine to handle input validation
+ // and act on the input
+ super.processLine();
+
+ // Reset the history scroll so the user sees the current
+ // input
+ this.#historyIndex = -1;
+ }
+
+ #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':
+ NativeConsole.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);
+ }
+
+ render() {
+ // Prevent the cursor from flashing while we render...
+ this.#terminal.hideCursor();
+ this.#terminal.cursorTo(0);
+ this.#terminal.clearScreenDown();
+
+ const { inputPrompt, line } = this;
+ this.#terminal.print(inputPrompt, line);
+ this.#terminal.cursorTo(inputPrompt.length + this.cursor);
+ this.#terminal.showCursor();
+
+ this.#terminal.commit();
+
+ this.emit('render');
+ }
+
+ /**
+ * @param {string[]} strings strings to write to stdout
+ */
+ print(...strings) {
+ this.#terminal.print(...strings);
+ this.#terminal.newLine();
+ this.#terminal.commit();
+ }
+
+ cancel() {
+ super.cancel();
+
+ this.#terminal.stopInput();
+ this.#terminal.newLine();
+ this.#terminal.commit();
+ }
+
+ prompt() {
+ super.prompt();
+
+ this.render();
+
+ // Start the async read loop...
+ this.#terminal.startInput();
+ }
+}
+
diff --git a/modules/esm/repl.js b/modules/esm/repl.js
new file mode 100644
index 00000000..798624b5
--- /dev/null
+++ b/modules/esm/repl.js
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com>
+
+import gi from 'gi';
+
+import { setRawMode, Readline } from './readline.js';
+import { prettyPrint } from './_prettyPrint.js';
+
+const NativeConsole = import.meta.importSync('_consoleNative');
+const system = import.meta.importSync('system');
+
+function runMainloop() {
+ imports.mainloop.run('repl');
+}
+
+function quitMainloop() {
+ imports.mainloop.quit('repl');
+}
+
+export class Repl {
+ #lineNumber = 0;
+
+ /** @type {string} */
+ #version;
+
+ constructor() {
+ this.#version = system.versionString;
+ }
+
+ [Symbol.toStringTag]() {
+ return 'Repl';
+ }
+
+ get lineNumber() {
+ return this.#lineNumber;
+ }
+
+ #print(string) {
+ this.readline.print(`${string}`);
+ }
+
+ #evaluateInternal(lines) {
+ try {
+ const result = NativeConsole.eval(lines, this.#lineNumber);
+
+ this.#print(`${prettyPrint(result)}`);
+
+ return null;
+ } catch (error) {
+ return error;
+ }
+ }
+
+ #printError(error) {
+ if (error.message)
+ this.#print(`Uncaught ${error.name}: ${error.message}`);
+ else this.#print(`${prettyPrint(error)}`);
+ }
+
+ #isValidInput(input) {
+ return NativeConsole.isValid(input);
+ }
+
+ evaluateInput(lines) {
+ this.#lineNumber++;
+
+ // Rough object/code block detection similar to Node
+ let trimmedLines = lines.trim();
+ if (trimmedLines.startsWith('{') && !trimmedLines.endsWith(';')) {
+ let wrappedLines = `(${trimmedLines})\n`;
+
+ // Attempt to evaluate any object literals in () first
+ let error = this.#evaluateInternal(wrappedLines);
+ if (error) this.#printError(error);
+
+ return;
+ }
+
+ let error = this.#evaluateInternal(lines);
+ if (!error) return;
+
+ this.#printError(error);
+ }
+
+ #startInput() {
+ this.readline.connect('validate', (_, line, input) => {
+ const isValid = this.#isValidInput(input);
+
+ if (isValid && this.readline.hasMultilineInput()) {
+ this.readline.completeMultilineInput();
+ return;
+ }
+
+ if (!isValid) {
+ this.readline.addLineToMultilineInput(line);
+ }
+ });
+
+ this.readline.connect('line', (_, line) => {
+ this.evaluateInput(line);
+ });
+
+ this.readline.connect('multiline', (_, multiline) => {
+ this.evaluateInput(multiline);
+ });
+
+ this.readline.connect('exit', () => {
+ this.exit();
+ });
+
+ this.readline.print(`GJS v${this.#version}`);
+ this.readline.prompt();
+ }
+
+ start() {
+ const prompt = '> ';
+ const pendingPrompt = '... ';
+
+ this.readline = Readline.create({ prompt, pendingPrompt });
+
+ const {isRaw, isAsync} = this.readline;
+
+
+ try {
+ if (isRaw) setRawMode(true);
+
+ this.#startInput();
+
+ if (isAsync) runMainloop();
+ } finally {
+ if (isRaw) setRawMode(false);
+ }
+ }
+
+ exit() {
+ try {
+ this.readline.cancel();
+
+ if (this.readline.isAsync) {
+ quitMainloop();
+ }
+ } catch {
+ // Force an exit if an error occurs
+ imports.system.exit(1);
+ }
+ }
+}
+
+// Install the Repl class in imports.console for backwards compatibility
+imports.console.Repl = Repl;
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
index 2f3f71d8..34854da5 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -67,7 +67,7 @@ class InternalModuleLoader {
/**
* @param {typeof globalThis} global the global object to handle module
* resolution
- * @param {(string, string) => import("../types").Module} compileFunc the
+ * @param {(string, string) => Module} compileFunc the
* function to compile a source into a module for a particular global
* object. Should be compileInternalModule() for InternalModuleLoader,
* but overridden in ModuleLoader
@@ -154,12 +154,12 @@ class InternalModuleLoader {
/**
* @param {string} specifier the specifier (e.g. relative path, root package) to resolve
- * @param {string | null} importingModuleURI the URI of the module
+ * @param {ModulePrivate} importingModule the private object of the module
* triggering this resolve
*
* @returns {Module | null}
*/
- resolveModule(specifier, importingModuleURI) {
+ resolveModule(specifier, importingModule) {
const registry = getRegistry(this.global);
// Check if the module has already been loaded
@@ -168,7 +168,7 @@ class InternalModuleLoader {
return module;
// 1) Resolve path and URI-based imports.
- const uri = this.resolveSpecifier(specifier, importingModuleURI);
+ const uri = this.resolveSpecifier(specifier, importingModule.uri);
if (uri) {
module = registry.get(uri.uri);
@@ -180,7 +180,7 @@ class InternalModuleLoader {
if (!result)
return null;
- const [text, internal = false] = result;
+ const [text, internal = importingModule.internal] = result;
const priv = new ModulePrivate(uri.uri, uri.uri, internal);
const compiled = this.compileModule(priv, text);
@@ -193,7 +193,7 @@ class InternalModuleLoader {
}
moduleResolveHook(importingModulePriv, specifier) {
- const resolved = this.resolveModule(specifier, importingModulePriv.uri ?? null);
+ const resolved = this.resolveModule(specifier, importingModulePriv);
if (!resolved)
throw new ImportError(`Module not found: ${specifier}`);
@@ -300,7 +300,7 @@ class ModuleLoader extends InternalModuleLoader {
* erroring if no resource is found.
*
* @param {string} specifier the module specifier to resolve for an import
- * @returns {import("./internalLoader").Module}
+ * @returns {Module}
*/
resolveBareSpecifier(specifier) {
// 2) Resolve internal imports.
@@ -332,10 +332,10 @@ class ModuleLoader extends InternalModuleLoader {
* @param {ModulePrivate} importingModulePriv
* the private object of the module initiating the import
* @param {string} specifier the module specifier to resolve for an import
- * @returns {import("./internalLoader").Module}
+ * @returns {Module}
*/
moduleResolveHook(importingModulePriv, specifier) {
- const module = this.resolveModule(specifier, importingModulePriv.uri);
+ const module = this.resolveModule(specifier, importingModulePriv);
if (module)
return module;
@@ -348,18 +348,17 @@ class ModuleLoader extends InternalModuleLoader {
if (!importingModulePriv || !importingModulePriv.uri)
throw new ImportError('Cannot resolve relative imports from an unknown file.');
- return this.resolveModuleAsync(specifier, importingModulePriv.uri);
+ return this.resolveModuleAsync(specifier, importingModulePriv);
}
/**
* Resolves a module import with optional handling for relative imports asynchronously.
*
* @param {string} specifier the specifier (e.g. relative path, root package) to resolve
- * @param {string | null} importingModuleURI the URI of the module
- * triggering this resolve
- * @returns {import("../types").Module}
+ * @param {ModulePrivate | null} importingModule the private object of the module triggering this resolve
+ * @returns {Module}
*/
- async resolveModuleAsync(specifier, importingModuleURI) {
+ async resolveModuleAsync(specifier, importingModule) {
const registry = getRegistry(this.global);
// Check if the module has already been loaded
@@ -368,7 +367,7 @@ class ModuleLoader extends InternalModuleLoader {
return module;
// 1) Resolve path and URI-based imports.
- const uri = this.resolveSpecifier(specifier, importingModuleURI);
+ const uri = this.resolveSpecifier(specifier, importingModule.uri);
if (uri) {
module = registry.get(uri.uri);
@@ -385,7 +384,7 @@ class ModuleLoader extends InternalModuleLoader {
if (module)
return module;
- const [text, internal = false] = result;
+ const [text, internal = importingModule.internal] = result;
const priv = new ModulePrivate(uri.uri, uri.uri, internal);
const compiled = this.compileModule(priv, text);
diff --git a/modules/modules.cpp b/modules/modules.cpp
index 987aa51f..5ae299be 100644
--- a/modules/modules.cpp
+++ b/modules/modules.cpp
@@ -20,6 +20,6 @@ void gjs_register_static_modules(void) {
registry.add("cairoNative", gjs_js_define_cairo_stuff);
#endif
registry.add("system", gjs_js_define_system_stuff);
- registry.add("console", gjs_define_console_stuff);
+ registry.add("_consoleNative", gjs_define_console_private_stuff);
registry.add("_print", gjs_define_print_stuff);
}
diff --git a/modules/script/_bootstrap/default.js b/modules/script/_bootstrap/default.js
index 59c24930..428218f5 100644
--- a/modules/script/_bootstrap/default.js
+++ b/modules/script/_bootstrap/default.js
@@ -1,169 +1,3 @@
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2013 Red Hat, Inc.
// SPDX-FileCopyrightText: 2020 Evan Welsh <contact@evanwelsh.com>
-
-(function (exports) {
- 'use strict';
-
- const {
- print,
- printerr,
- log: nativeLog,
- logError: nativeLogError,
- setPrettyPrintFunction,
- } = imports._print;
-
- function log(...args) {
- return nativeLog(args.map(arg => typeof arg === 'string' ? arg : prettyPrint(arg)).join(' '));
- }
-
- function logError(e, ...args) {
- if (args.length === 0)
- return nativeLogError(e);
- return nativeLogError(e, args.map(arg => typeof arg === 'string' ? arg : prettyPrint(arg)).join(' '));
- }
-
- function prettyPrint(value) {
- switch (typeof value) {
- case 'object':
- if (value.toString === Object.prototype.toString ||
- value.toString === Array.prototype.toString ||
- value.toString === Date.prototype.toString) {
- const printedObjects = new WeakSet();
- return formatObject(value, printedObjects);
- }
- // If the object has a nonstandard toString, prefer that
- return value.toString();
- case 'function':
- if (value.toString === Function.prototype.toString)
- return formatFunction(value);
- return value.toString();
- case 'string':
- return JSON.stringify(value);
- case 'symbol':
- return formatSymbol(value);
- default:
- return value.toString();
- }
- }
-
- function formatPropertyKey(key) {
- if (typeof key === 'symbol')
- return `[${formatSymbol(key)}]`;
- return `${key}`;
- }
-
- function formatObject(obj, printedObjects) {
- printedObjects.add(obj);
- if (Array.isArray(obj))
- return formatArray(obj, printedObjects).toString();
-
- if (obj instanceof Date)
- return formatDate(obj);
-
- if (obj[Symbol.toStringTag] === 'GIRepositoryNamespace')
- return obj.toString();
-
- const formattedObject = [];
- const keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
- for (const propertyKey of keys) {
- const value = obj[propertyKey];
- const key = formatPropertyKey(propertyKey);
- switch (typeof value) {
- case 'object':
- if (printedObjects.has(value))
- formattedObject.push(`${key}: [Circular]`);
- else
- formattedObject.push(`${key}: ${formatObject(value, printedObjects)}`);
- break;
- case 'function':
- formattedObject.push(`${key}: ${formatFunction(value)}`);
- break;
- case 'string':
- formattedObject.push(`${key}: "${value}"`);
- break;
- case 'symbol':
- formattedObject.push(`${key}: ${formatSymbol(value)}`);
- break;
- default:
- formattedObject.push(`${key}: ${value}`);
- break;
- }
- }
- return Object.keys(formattedObject).length === 0 ? '{}'
- : `{ ${formattedObject.join(', ')} }`;
- }
-
- function formatArray(arr, printedObjects) {
- const formattedArray = [];
- for (const [key, value] of arr.entries()) {
- if (printedObjects.has(value))
- formattedArray[key] = '[Circular]';
- else
- formattedArray[key] = prettyPrint(value);
- }
- return `[${formattedArray.join(', ')}]`;
- }
-
- function formatDate(date) {
- return date.toISOString();
- }
-
- function formatFunction(func) {
- let funcOutput = `[ Function: ${func.name} ]`;
- return funcOutput;
- }
-
- function formatSymbol(sym) {
- // Try to format Symbols in the same way that they would be constructed.
-
- // First check if this is a global registered symbol
- const globalKey = Symbol.keyFor(sym);
- if (globalKey !== undefined)
- return `Symbol.for("${globalKey}")`;
-
- const descr = sym.description;
- // Special-case the 'well-known' (built-in) Symbols
- if (descr.startsWith('Symbol.'))
- return descr;
-
- // Otherwise, it's just a regular symbol
- return `Symbol("${descr}")`;
- }
-
- Object.defineProperties(exports, {
- ARGV: {
- configurable: false,
- enumerable: true,
- get() {
- // Wait until after bootstrap or programArgs won't be set.
- return imports.system.programArgs;
- },
- },
- print: {
- configurable: false,
- enumerable: true,
- writable: true,
- value: print,
- },
- printerr: {
- configurable: false,
- enumerable: true,
- writable: true,
- value: printerr,
- },
- log: {
- configurable: false,
- enumerable: true,
- writable: true,
- value: log,
- },
- logError: {
- configurable: false,
- enumerable: true,
- writable: true,
- value: logError,
- },
- });
- setPrettyPrintFunction(exports, prettyPrint);
-})(globalThis);
diff --git a/modules/script/console.js b/modules/script/console.js
new file mode 100644
index 00000000..d80b699c
--- /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();
+}