[gjs/ewlsh/nova-repl: 3/7] Implement non-blocking Repl with Mainloop




commit 87411c63a6db2dd00eda7ff56a16c1713d6ebed8
Author: Evan Welsh <contact evanwelsh com>
Date:   Thu Sep 2 22:37:05 2021 -0700

    Implement non-blocking Repl with Mainloop

 .eslintignore                  |   1 +
 .gitlab-ci.yml                 |   2 +-
 gjs/console.cpp                |  20 +-
 js.gresource.xml               |   5 +
 meson.build                    |   3 +-
 modules/console.cpp            | 112 ++++-
 modules/console.h              |   4 +
 modules/esm/.eslintrc.yml      |   2 +-
 modules/esm/_bootstrap/repl.js |  10 +
 modules/esm/_repl/cliffy.js    | 974 +++++++++++++++++++++++++++++++++++++++++
 modules/esm/_repl/deno.js      |  11 +
 modules/esm/repl.js            | 634 +++++++++++++++++++++++++++
 modules/modules.cpp            |   2 +
 test/check-pch.sh              |   1 +
 tools/cliffy/.eslintrc.yml     |   7 +
 tools/cliffy/ansi.js           |   8 +
 tools/cliffy/bundle.sh         |   7 +
 tools/cliffy/lib.js            |  31 ++
 tools/cliffy/transform.js      |  24 +
 tools/run_iwyu.sh              |   1 +
 util/console.cpp               |  81 +++-
 util/console.hxx               |  27 ++
 22 files changed, 1950 insertions(+), 17 deletions(-)
---
diff --git a/.eslintignore b/.eslintignore
index 9ee950d3..b6ad5ab5 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -4,3 +4,4 @@
 installed-tests/js/jasmine.js
 installed-tests/js/modules/badOverrides/WarnLib.js
 modules/script/jsUnit.js
+modules/esm/_repl/cliffy.js
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cc531dad..5615d3e4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -273,7 +273,7 @@ codespell:
   stage: source_check
   script:
     - codespell --version
-    - codespell -S "*.png,*.po,*.jpg,*.wrap,.git,LICENSES" -f --builtin "code,usage,clear" 
--skip="./installed-tests/js/jasmine.js,./README.md,./build/flatpak/*.json" 
--ignore-words-list="afterall,befores,files',filetest,gir,inout,stdio,uint,upto,xdescribe"
+    - codespell -S "*.png,*.po,*.jpg,*.wrap,.git,LICENSES" -f --builtin "code,usage,clear" 
--skip="./installed-tests/js/jasmine.js,./README.md,./build/flatpak/*.json,./modules/esm/_repl/cliffy.js" 
--ignore-words-list="afterall,befores,files',filetest,gir,inout,stdio,uint,upto,xdescribe"
   except:
     - schedules
 
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 49c82299..632416f6 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/js.gresource.xml b/js.gresource.xml
index a730f2b8..a5c057b0 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -9,15 +9,20 @@
 
     <!-- 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>
     <file>modules/esm/_encoding/util.js</file>
+
+    <file>modules/esm/_repl/deno.js</file>
+    <file>modules/esm/_repl/cliffy.js</file>
   
     <file>modules/esm/cairo.js</file>
     <file>modules/esm/gettext.js</file>
     <file>modules/esm/console.js</file>
     <file>modules/esm/gi.js</file>
+    <file>modules/esm/repl.js</file>
     <file>modules/esm/system.js</file>
 
     <!-- Script-based Modules -->
diff --git a/meson.build b/meson.build
index b6f6b40e..795ee985 100644
--- a/meson.build
+++ b/meson.build
@@ -306,6 +306,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_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',
     required: build_profiler))
@@ -428,7 +429,7 @@ libgjs_jsapi_sources = [
     'gjs/jsapi-util-root.h',
     'gjs/jsapi-util-string.cpp',
     'gjs/jsapi-util.cpp', 'gjs/jsapi-util.h',
-    'util/console.cpp', 'util/console.h',
+    'util/console.cpp', 'util/console.h', 'util/console.hxx',
     'util/log.cpp', 'util/log.h',
     'util/misc.cpp', 'util/misc.h',
 ]
diff --git a/modules/console.cpp b/modules/console.cpp
index 393aed59..29d84437 100644
--- a/modules/console.cpp
+++ b/modules/console.cpp
@@ -40,8 +40,10 @@
 
 #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.hxx"
 
 namespace mozilla {
 union Utf8Unit;
@@ -146,13 +148,9 @@ sigjmp_buf AutoCatchCtrlC::jump_buffer;
     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,8 +159,8 @@ sigjmp_buf AutoCatchCtrlC::jump_buffer;
     JS::CompileOptions options(cx);
     options.setFileAndLine("typein", lineno);
 
-    JS::RootedValue result(cx);
-    if (!JS::Evaluate(cx, options, source, &result)) {
+    JS::RootedValue eval_result(cx);
+    if (!JS::Evaluate(cx, options, source, &eval_result)) {
         if (!JS_IsExceptionPending(cx))
             return false;
     }
@@ -170,6 +168,22 @@ sigjmp_buf AutoCatchCtrlC::jump_buffer;
     GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
     gjs->schedule_gc_if_needed();
 
+    result.set(eval_result);
+    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) {
+    JS::RootedValue result(cx);
+    if (!gjs_console_eval(cx, bytes, lineno, &result)) {
+        return false;
+    }
+
     if (result.isUndefined())
         return true;
 
@@ -274,6 +288,66 @@ gjs_console_interact(JSContext *context,
     return true;
 }
 
+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_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;
+    if (!gjs_parse_call_args(cx, "eval", args, "si", "expression", &expr,
+                             "lineNumber", &lineno))
+        return false;
+
+    bool ok;
+    {
+        AutoReportException are(cx);
+        ok = gjs_console_eval(cx, std::string(expr.get()), lineno, args.rval());
+    }
+    return ok;
+}
+
+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;
+
+    JS::RootedObject global(cx, gjs_get_import_global(cx));
+
+    args.rval().setBoolean(
+        JS_Utf8BufferIsCompilableUnit(cx, global, code.get(), code_len));
+    return true;
+}
+
 bool
 gjs_define_console_stuff(JSContext              *context,
                          JS::MutableHandleObject module)
@@ -284,3 +358,23 @@ gjs_define_console_stuff(JSContext              *context,
                                  gjs_console_interact, 1,
                                  GJS_MODULE_PROP_FLAGS);
 }
+
+static JSFunctionSpec console_module_funcs[] = {
+    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_FS_END,
+};
+
+bool gjs_define_console_private_stuff(JSContext* cx,
+                                      JS::MutableHandleObject module) {
+    module.set(JS_NewPlainObject(cx));
+    if (!module)
+        return false;
+
+    return JS_DefineFunctions(cx, module, console_module_funcs);
+}
diff --git a/modules/console.h b/modules/console.h
index 73ed4c0e..d63df22e 100644
--- a/modules/console.h
+++ b/modules/console.h
@@ -15,4 +15,8 @@ GJS_JSAPI_RETURN_CONVENTION
 bool gjs_define_console_stuff(JSContext              *context,
                               JS::MutableHandleObject module);
 
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_define_console_private_stuff(JSContext* context,
+                                      JS::MutableHandleObject module);
+
 #endif  // MODULES_CONSOLE_H_
diff --git a/modules/esm/.eslintrc.yml b/modules/esm/.eslintrc.yml
index 84e48aef..2925b05e 100644
--- a/modules/esm/.eslintrc.yml
+++ b/modules/esm/.eslintrc.yml
@@ -1,6 +1,6 @@
 ---
 # SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
-# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+# SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
 extends: '../../.eslintrc.yml'
 parserOptions:
   sourceType: 'module'
diff --git a/modules/esm/_bootstrap/repl.js b/modules/esm/_bootstrap/repl.js
new file mode 100644
index 00000000..fa51a1fb
--- /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.run();
diff --git a/modules/esm/_repl/cliffy.js b/modules/esm/_repl/cliffy.js
new file mode 100644
index 00000000..844ef1ad
--- /dev/null
+++ b/modules/esm/_repl/cliffy.js
@@ -0,0 +1,974 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: 2020 Benjamin Fischer <c4spar gmx de>
+
+// Inject our shim for the Deno global...
+import * as Deno from './deno.js';
+let globalThis = { Deno };
+const base64abc = [
+    "A",
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "G",
+    "H",
+    "I",
+    "J",
+    "K",
+    "L",
+    "M",
+    "N",
+    "O",
+    "P",
+    "Q",
+    "R",
+    "S",
+    "T",
+    "U",
+    "V",
+    "W",
+    "X",
+    "Y",
+    "Z",
+    "a",
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g",
+    "h",
+    "i",
+    "j",
+    "k",
+    "l",
+    "m",
+    "n",
+    "o",
+    "p",
+    "q",
+    "r",
+    "s",
+    "t",
+    "u",
+    "v",
+    "w",
+    "x",
+    "y",
+    "z",
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "+",
+    "/"
+];
+function encode(data) {
+    const uint8 = typeof data === "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? 
data : new Uint8Array(data);
+    let result = "", i;
+    const l = uint8.length;
+    for(i = 2; i < l; i += 3){
+        result += base64abc[uint8[i - 2] >> 2];
+        result += base64abc[(uint8[i - 2] & 3) << 4 | uint8[i - 1] >> 4];
+        result += base64abc[(uint8[i - 1] & 15) << 2 | uint8[i] >> 6];
+        result += base64abc[uint8[i] & 63];
+    }
+    if (i === l + 1) {
+        result += base64abc[uint8[i - 2] >> 2];
+        result += base64abc[(uint8[i - 2] & 3) << 4];
+        result += "==";
+    }
+    if (i === l) {
+        result += base64abc[uint8[i - 2] >> 2];
+        result += base64abc[(uint8[i - 2] & 3) << 4 | uint8[i - 1] >> 4];
+        result += base64abc[(uint8[i - 1] & 15) << 2];
+        result += "=";
+    }
+    return result;
+}
+const ESC = "\x1B";
+const CSI = `${ESC}[`;
+const OSC = `${ESC}]`;
+const SEP = ";";
+const bel = "\u0007";
+const cursorPosition = `${CSI}6n`;
+function cursorTo(x, y) {
+    if (typeof y !== "number") {
+        return `${CSI}${x}G`;
+    }
+    return `${CSI}${y};${x}H`;
+}
+function cursorMove(x, y) {
+    let ret = "";
+    if (x < 0) {
+        ret += `${CSI}${-x}D`;
+    } else if (x > 0) {
+        ret += `${CSI}${x}C`;
+    }
+    if (y < 0) {
+        ret += `${CSI}${-y}A`;
+    } else if (y > 0) {
+        ret += `${CSI}${y}B`;
+    }
+    return ret;
+}
+function cursorUp(count = 1) {
+    return `${CSI}${count}A`;
+}
+function cursorDown(count = 1) {
+    return `${CSI}${count}B`;
+}
+function cursorForward(count = 1) {
+    return `${CSI}${count}C`;
+}
+function cursorBackward(count = 1) {
+    return `${CSI}${count}D`;
+}
+function cursorNextLine(count = 1) {
+    return `${CSI}E`.repeat(count);
+}
+function cursorPrevLine(count = 1) {
+    return `${CSI}F`.repeat(count);
+}
+const cursorLeft = `${CSI}G`;
+const cursorHide = `${CSI}?25l`;
+const cursorShow = `${CSI}?25h`;
+const cursorSave = `${ESC}7`;
+const cursorRestore = `${ESC}8`;
+function scrollUp(count = 1) {
+    return `${CSI}S`.repeat(count);
+}
+function scrollDown(count = 1) {
+    return `${CSI}T`.repeat(count);
+}
+const eraseScreen = `${CSI}2J`;
+function eraseUp(count = 1) {
+    return `${CSI}1J`.repeat(count);
+}
+function eraseDown(count = 1) {
+    return `${CSI}0J`.repeat(count);
+}
+const eraseLine = `${CSI}2K`;
+const eraseLineEnd = `${CSI}0K`;
+const eraseLineStart = `${CSI}1K`;
+function eraseLines(count) {
+    let clear = "";
+    for(let i = 0; i < count; i++){
+        clear += eraseLine + (i < count - 1 ? cursorUp() : "");
+    }
+    clear += cursorLeft;
+    return clear;
+}
+const clearScreen = "\u001Bc";
+const clearTerminal = Deno.build.os === "windows" ? `${eraseScreen}${CSI}0f` : 
`${eraseScreen}${CSI}3J${CSI}H`;
+function link(text, url) {
+    return [
+        OSC,
+        "8",
+        SEP,
+        SEP,
+        url,
+        bel,
+        text,
+        OSC,
+        "8",
+        SEP,
+        SEP,
+        bel, 
+    ].join("");
+}
+function image(buffer, options) {
+    let ret = `${OSC}1337;File=inline=1`;
+    if (options?.width) {
+        ret += `;width=${options.width}`;
+    }
+    if (options?.height) {
+        ret += `;height=${options.height}`;
+    }
+    if (options?.preserveAspectRatio === false) {
+        ret += ";preserveAspectRatio=0";
+    }
+    return ret + ":" + encode(buffer) + bel;
+}
+const mod = function() {
+    return {
+        bel: bel,
+        cursorPosition: cursorPosition,
+        cursorTo: cursorTo,
+        cursorMove: cursorMove,
+        cursorUp: cursorUp,
+        cursorDown: cursorDown,
+        cursorForward: cursorForward,
+        cursorBackward: cursorBackward,
+        cursorNextLine: cursorNextLine,
+        cursorPrevLine: cursorPrevLine,
+        cursorLeft: cursorLeft,
+        cursorHide: cursorHide,
+        cursorShow: cursorShow,
+        cursorSave: cursorSave,
+        cursorRestore: cursorRestore,
+        scrollUp: scrollUp,
+        scrollDown: scrollDown,
+        eraseScreen: eraseScreen,
+        eraseUp: eraseUp,
+        eraseDown: eraseDown,
+        eraseLine: eraseLine,
+        eraseLineEnd: eraseLineEnd,
+        eraseLineStart: eraseLineStart,
+        eraseLines: eraseLines,
+        clearScreen: clearScreen,
+        clearTerminal: clearTerminal,
+        link: link,
+        image: image
+    };
+}();
+const ansi = factory1();
+function factory1() {
+    let result = [];
+    let stack = [];
+    const ansi1 = function(...args) {
+        if (this) {
+            if (args.length) {
+                update(args);
+                return this;
+            }
+            return this.toString();
+        }
+        return factory1();
+    };
+    ansi1.text = function(text) {
+        stack.push([
+            text,
+            []
+        ]);
+        return this;
+    };
+    ansi1.toString = function() {
+        update();
+        const str = result.join("");
+        result = [];
+        return str;
+    };
+    ansi1.toBuffer = function() {
+        return new TextEncoder().encode(this.toString());
+    };
+    const methodList = Object.entries(mod);
+    for (const [name, method] of methodList){
+        Object.defineProperty(ansi1, name, {
+            get () {
+                stack.push([
+                    method,
+                    []
+                ]);
+                return this;
+            }
+        });
+    }
+    return ansi1;
+    function update(args) {
+        if (!stack.length) {
+            return;
+        }
+        if (args) {
+            stack[stack.length - 1][1] = args;
+        }
+        result.push(...stack.map(([prop, args1])=>typeof prop === "string" ? prop : prop.call(ansi1, 
...args1)
+        ));
+        stack = [];
+    }
+}
+const { Deno: Deno1  } = globalThis;
+const noColor = typeof Deno1?.noColor === "boolean" ? Deno1.noColor : true;
+let enabled = !noColor;
+function setColorEnabled(value) {
+    if (noColor) {
+        return;
+    }
+    enabled = value;
+}
+function getColorEnabled() {
+    return enabled;
+}
+function code(open, close) {
+    return {
+        open: `\x1b[${open.join(";")}m`,
+        close: `\x1b[${close}m`,
+        regexp: new RegExp(`\\x1b\\[${close}m`, "g")
+    };
+}
+function run(str, code1) {
+    return enabled ? `${code1.open}${str.replace(code1.regexp, code1.open)}${code1.close}` : str;
+}
+function reset(str) {
+    return run(str, code([
+        0
+    ], 0));
+}
+function bold(str) {
+    return run(str, code([
+        1
+    ], 22));
+}
+function dim(str) {
+    return run(str, code([
+        2
+    ], 22));
+}
+function italic(str) {
+    return run(str, code([
+        3
+    ], 23));
+}
+function underline(str) {
+    return run(str, code([
+        4
+    ], 24));
+}
+function inverse(str) {
+    return run(str, code([
+        7
+    ], 27));
+}
+function hidden(str) {
+    return run(str, code([
+        8
+    ], 28));
+}
+function strikethrough(str) {
+    return run(str, code([
+        9
+    ], 29));
+}
+function black(str) {
+    return run(str, code([
+        30
+    ], 39));
+}
+function red(str) {
+    return run(str, code([
+        31
+    ], 39));
+}
+function green(str) {
+    return run(str, code([
+        32
+    ], 39));
+}
+function yellow(str) {
+    return run(str, code([
+        33
+    ], 39));
+}
+function blue(str) {
+    return run(str, code([
+        34
+    ], 39));
+}
+function magenta(str) {
+    return run(str, code([
+        35
+    ], 39));
+}
+function cyan(str) {
+    return run(str, code([
+        36
+    ], 39));
+}
+function white(str) {
+    return run(str, code([
+        37
+    ], 39));
+}
+function gray(str) {
+    return brightBlack(str);
+}
+function brightBlack(str) {
+    return run(str, code([
+        90
+    ], 39));
+}
+function brightRed(str) {
+    return run(str, code([
+        91
+    ], 39));
+}
+function brightGreen(str) {
+    return run(str, code([
+        92
+    ], 39));
+}
+function brightYellow(str) {
+    return run(str, code([
+        93
+    ], 39));
+}
+function brightBlue(str) {
+    return run(str, code([
+        94
+    ], 39));
+}
+function brightMagenta(str) {
+    return run(str, code([
+        95
+    ], 39));
+}
+function brightCyan(str) {
+    return run(str, code([
+        96
+    ], 39));
+}
+function brightWhite(str) {
+    return run(str, code([
+        97
+    ], 39));
+}
+function bgBlack(str) {
+    return run(str, code([
+        40
+    ], 49));
+}
+function bgRed(str) {
+    return run(str, code([
+        41
+    ], 49));
+}
+function bgGreen(str) {
+    return run(str, code([
+        42
+    ], 49));
+}
+function bgYellow(str) {
+    return run(str, code([
+        43
+    ], 49));
+}
+function bgBlue(str) {
+    return run(str, code([
+        44
+    ], 49));
+}
+function bgMagenta(str) {
+    return run(str, code([
+        45
+    ], 49));
+}
+function bgCyan(str) {
+    return run(str, code([
+        46
+    ], 49));
+}
+function bgWhite(str) {
+    return run(str, code([
+        47
+    ], 49));
+}
+function bgBrightBlack(str) {
+    return run(str, code([
+        100
+    ], 49));
+}
+function bgBrightRed(str) {
+    return run(str, code([
+        101
+    ], 49));
+}
+function bgBrightGreen(str) {
+    return run(str, code([
+        102
+    ], 49));
+}
+function bgBrightYellow(str) {
+    return run(str, code([
+        103
+    ], 49));
+}
+function bgBrightBlue(str) {
+    return run(str, code([
+        104
+    ], 49));
+}
+function bgBrightMagenta(str) {
+    return run(str, code([
+        105
+    ], 49));
+}
+function bgBrightCyan(str) {
+    return run(str, code([
+        106
+    ], 49));
+}
+function bgBrightWhite(str) {
+    return run(str, code([
+        107
+    ], 49));
+}
+function clampAndTruncate(n, max = 255, min = 0) {
+    return Math.trunc(Math.max(Math.min(n, max), min));
+}
+function rgb8(str, color) {
+    return run(str, code([
+        38,
+        5,
+        clampAndTruncate(color)
+    ], 39));
+}
+function bgRgb8(str, color) {
+    return run(str, code([
+        48,
+        5,
+        clampAndTruncate(color)
+    ], 49));
+}
+function rgb24(str, color) {
+    if (typeof color === "number") {
+        return run(str, code([
+            38,
+            2,
+            color >> 16 & 255,
+            color >> 8 & 255,
+            color & 255
+        ], 39));
+    }
+    return run(str, code([
+        38,
+        2,
+        clampAndTruncate(color.r),
+        clampAndTruncate(color.g),
+        clampAndTruncate(color.b), 
+    ], 39));
+}
+function bgRgb24(str, color) {
+    if (typeof color === "number") {
+        return run(str, code([
+            48,
+            2,
+            color >> 16 & 255,
+            color >> 8 & 255,
+            color & 255
+        ], 49));
+    }
+    return run(str, code([
+        48,
+        2,
+        clampAndTruncate(color.r),
+        clampAndTruncate(color.g),
+        clampAndTruncate(color.b), 
+    ], 49));
+}
+const ANSI_PATTERN = new RegExp([
+    "[\\u001B\\u009B][[\\]()$;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/$&.:=?%@~_]*)*)?\\u0007)",
+    "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", 
+].join("|"), "g");
+function stripColor(string) {
+    return string.replace(ANSI_PATTERN, "");
+}
+const mod1 = function() {
+    return {
+        setColorEnabled: setColorEnabled,
+        getColorEnabled: getColorEnabled,
+        reset: reset,
+        bold: bold,
+        dim: dim,
+        italic: italic,
+        underline: underline,
+        inverse: inverse,
+        hidden: hidden,
+        strikethrough: strikethrough,
+        black: black,
+        red: red,
+        green: green,
+        yellow: yellow,
+        blue: blue,
+        magenta: magenta,
+        cyan: cyan,
+        white: white,
+        gray: gray,
+        brightBlack: brightBlack,
+        brightRed: brightRed,
+        brightGreen: brightGreen,
+        brightYellow: brightYellow,
+        brightBlue: brightBlue,
+        brightMagenta: brightMagenta,
+        brightCyan: brightCyan,
+        brightWhite: brightWhite,
+        bgBlack: bgBlack,
+        bgRed: bgRed,
+        bgGreen: bgGreen,
+        bgYellow: bgYellow,
+        bgBlue: bgBlue,
+        bgMagenta: bgMagenta,
+        bgCyan: bgCyan,
+        bgWhite: bgWhite,
+        bgBrightBlack: bgBrightBlack,
+        bgBrightRed: bgBrightRed,
+        bgBrightGreen: bgBrightGreen,
+        bgBrightYellow: bgBrightYellow,
+        bgBrightBlue: bgBrightBlue,
+        bgBrightMagenta: bgBrightMagenta,
+        bgBrightCyan: bgBrightCyan,
+        bgBrightWhite: bgBrightWhite,
+        rgb8: rgb8,
+        bgRgb8: bgRgb8,
+        rgb24: rgb24,
+        bgRgb24: bgRgb24,
+        stripColor: stripColor
+    };
+}();
+const proto = Object.create(null);
+const methodNames = Object.keys(mod1);
+for (const name of methodNames){
+    if (name === "setColorEnabled" || name === "getColorEnabled") {
+        continue;
+    }
+    Object.defineProperty(proto, name, {
+        get () {
+            return factory2([
+                ...this._stack,
+                name
+            ]);
+        }
+    });
+}
+const colors = factory2();
+function factory2(stack = []) {
+    const colors1 = function(str, ...args) {
+        if (str) {
+            const lastIndex = stack.length - 1;
+            return stack.reduce((str1, name1, index)=>index === lastIndex ? mod1[name1](str1, ...args) : 
mod1[name1](str1)
+            , str);
+        }
+        const tmp = stack.slice();
+        stack = [];
+        return factory2(tmp);
+    };
+    Object.setPrototypeOf(colors1, proto);
+    colors1._stack = stack;
+    return colors1;
+}
+function getCursorPosition2({ stdin =Deno.stdin , stdout =Deno.stdout  } = {
+}) {
+    const data = new Uint8Array(8);
+    Deno.setRaw(stdin.rid, true);
+    stdout.writeSync(new TextEncoder().encode(cursorPosition));
+    stdin.readSync(data);
+    Deno.setRaw(stdin.rid, false);
+    const [y, x] = new TextDecoder().decode(data).match(/\[(\d+);(\d+)R/)?.slice(1, 3).map(Number) ?? [
+        0,
+        0
+    ];
+    return {
+        x,
+        y
+    };
+}
+const mod2 = function() {
+    return {
+        ansi,
+        bel,
+        cursorPosition,
+        cursorTo,
+        cursorMove,
+        cursorUp,
+        cursorDown,
+        cursorForward,
+        cursorBackward,
+        cursorNextLine,
+        cursorPrevLine,
+        cursorLeft,
+        cursorHide,
+        cursorShow,
+        cursorSave,
+        cursorRestore,
+        scrollUp,
+        scrollDown,
+        eraseScreen,
+        eraseUp,
+        eraseDown,
+        eraseLine,
+        eraseLineEnd,
+        eraseLineStart,
+        eraseLines,
+        clearScreen,
+        clearTerminal,
+        link,
+        image,
+        colors,
+        getCursorPosition: getCursorPosition2,
+        mod
+    };
+}();
+const KeyMap = {
+    "[P": "f1",
+    "[Q": "f2",
+    "[R": "f3",
+    "[S": "f4",
+    "OP": "f1",
+    "OQ": "f2",
+    "OR": "f3",
+    "OS": "f4",
+    "[11~": "f1",
+    "[12~": "f2",
+    "[13~": "f3",
+    "[14~": "f4",
+    "[[A": "f1",
+    "[[B": "f2",
+    "[[C": "f3",
+    "[[D": "f4",
+    "[[E": "f5",
+    "[15~": "f5",
+    "[17~": "f6",
+    "[18~": "f7",
+    "[19~": "f8",
+    "[20~": "f9",
+    "[21~": "f10",
+    "[23~": "f11",
+    "[24~": "f12",
+    "[A": "up",
+    "[B": "down",
+    "[C": "right",
+    "[D": "left",
+    "[E": "clear",
+    "[F": "end",
+    "[H": "home",
+    "OA": "up",
+    "OB": "down",
+    "OC": "right",
+    "OD": "left",
+    "OE": "clear",
+    "OF": "end",
+    "OH": "home",
+    "[1~": "home",
+    "[2~": "insert",
+    "[3~": "delete",
+    "[4~": "end",
+    "[5~": "pageup",
+    "[6~": "pagedown",
+    "[[5~": "pageup",
+    "[[6~": "pagedown",
+    "[7~": "home",
+    "[8~": "end"
+};
+const KeyMapShift = {
+    "[a": "up",
+    "[b": "down",
+    "[c": "right",
+    "[d": "left",
+    "[e": "clear",
+    "[2$": "insert",
+    "[3$": "delete",
+    "[5$": "pageup",
+    "[6$": "pagedown",
+    "[7$": "home",
+    "[8$": "end",
+    "[Z": "tab"
+};
+const KeyMapCtrl = {
+    "Oa": "up",
+    "Ob": "down",
+    "Oc": "right",
+    "Od": "left",
+    "Oe": "clear",
+    "[2^": "insert",
+    "[3^": "delete",
+    "[5^": "pageup",
+    "[6^": "pagedown",
+    "[7^": "home",
+    "[8^": "end"
+};
+const SpecialKeyMap = {
+    "\r": "return",
+    "\n": "enter",
+    "\t": "tab",
+    "\b": "backspace",
+    "\x7f": "backspace",
+    "\x1b": "escape",
+    " ": "space"
+};
+const kEscape = "\x1b";
+function parse(data) {
+    let index = -1;
+    const keys = [];
+    const input = data instanceof Uint8Array ? new TextDecoder().decode(data) : data;
+    const hasNext = ()=>input.length - 1 >= index + 1
+    ;
+    const next = ()=>input[++index]
+    ;
+    parseNext();
+    return keys;
+    function parseNext() {
+        let ch = next();
+        let s = ch;
+        let escaped = false;
+        const key = {
+            name: undefined,
+            sequence: undefined,
+            code: undefined,
+            ctrl: false,
+            meta: false,
+            shift: false
+        };
+        if (ch === kEscape && hasNext()) {
+            escaped = true;
+            s += ch = next();
+            if (ch === kEscape) {
+                s += ch = next();
+            }
+        }
+        if (escaped && (ch === "O" || ch === "[")) {
+            let code1 = ch;
+            let modifier = 0;
+            if (ch === "O") {
+                s += ch = next();
+                if (ch >= "0" && ch <= "9") {
+                    modifier = (Number(ch) >> 0) - 1;
+                    s += ch = next();
+                }
+                code1 += ch;
+            } else if (ch === "[") {
+                s += ch = next();
+                if (ch === "[") {
+                    code1 += ch;
+                    s += ch = next();
+                }
+                const cmdStart = s.length - 1;
+                if (ch >= "0" && ch <= "9") {
+                    s += ch = next();
+                    if (ch >= "0" && ch <= "9") {
+                        s += ch = next();
+                    }
+                }
+                if (ch === ";") {
+                    s += ch = next();
+                    if (ch >= "0" && ch <= "9") {
+                        s += next();
+                    }
+                }
+                const cmd = s.slice(cmdStart);
+                let match;
+                if (match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/)) {
+                    code1 += match[1] + match[4];
+                    modifier = (Number(match[3]) || 1) - 1;
+                } else if (match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/)) {
+                    code1 += match[4];
+                    modifier = (Number(match[3]) || 1) - 1;
+                } else {
+                    code1 += cmd;
+                }
+            }
+            key.ctrl = !!(modifier & 4);
+            key.meta = !!(modifier & 10);
+            key.shift = !!(modifier & 1);
+            key.code = code1;
+            if (code1 in KeyMap) {
+                key.name = KeyMap[code1];
+            } else if (code1 in KeyMapShift) {
+                key.name = KeyMapShift[code1];
+                key.shift = true;
+            } else if (code1 in KeyMapCtrl) {
+                key.name = KeyMapCtrl[code1];
+                key.ctrl = true;
+            } else {
+                key.name = "undefined";
+            }
+        } else if (ch in SpecialKeyMap) {
+            key.name = SpecialKeyMap[ch];
+            key.meta = escaped;
+        } else if (!escaped && ch <= "\x1a") {
+            key.name = String.fromCharCode(ch.charCodeAt(0) + "a".charCodeAt(0) - 1);
+            key.ctrl = true;
+        } else if (/^[0-9A-Za-z]$/.test(ch)) {
+            key.name = ch.toLowerCase();
+            key.shift = /^[A-Z]$/.test(ch);
+            key.meta = escaped;
+        } else if (escaped) {
+            key.name = ch.length ? undefined : "escape";
+            key.meta = true;
+        }
+        key.sequence = s;
+        if (s.length !== 0 && (key.name !== undefined || escaped)) {
+            keys.push(key);
+        } else if (charLengthAt(s, 0) === s.length) {
+            keys.push(key);
+        } else {
+            throw new Error("Unrecognized or broken escape sequence");
+        }
+        if (hasNext()) {
+            parseNext();
+        }
+    }
+}
+function charLengthAt(str, i) {
+    const pos = str.codePointAt(i);
+    if (typeof pos === "undefined") {
+        return 1;
+    }
+    return pos >= 65536 ? 2 : 1;
+}
+const mod3 = function() {
+    return {
+        parse,
+        KeyMap,
+        KeyMapShift,
+        KeyMapCtrl,
+        SpecialKeyMap
+    };
+}();
+const main = {
+    ARROW_UP: "↑",
+    ARROW_DOWN: "↓",
+    ARROW_LEFT: "←",
+    ARROW_RIGHT: "→",
+    ARROW_UP_LEFT: "↖",
+    ARROW_UP_RIGHT: "↗",
+    ARROW_DOWN_RIGHT: "↘",
+    ARROW_DOWN_LEFT: "↙",
+    RADIO_ON: "◉",
+    RADIO_OFF: "◯",
+    TICK: "✔",
+    CROSS: "✘",
+    ELLIPSIS: "…",
+    POINTER_SMALL: "›",
+    LINE: "─",
+    POINTER: "❯",
+    INFO: "ℹ",
+    TAB_LEFT: "⇤",
+    TAB_RIGHT: "⇥",
+    ESCAPE: "⎋",
+    BACKSPACE: "⌫",
+    PAGE_UP: "⇞",
+    PAGE_DOWN: "⇟",
+    ENTER: "↵",
+    SEARCH: "⌕"
+};
+const win = {
+    ...main,
+    RADIO_ON: "(*)",
+    RADIO_OFF: "( )",
+    TICK: "√",
+    CROSS: "×",
+    POINTER_SMALL: "»"
+};
+const Figures1 = Deno.build.os === "windows" ? win : main;
+export { Figures1 as Figures };
+function getCursorPosition1({ stdin , stdout  }) {
+    stdout.write_bytes(new TextEncoder().encode(cursorPosition), null);
+    const data = stdin.read_bytes(8, null).toArray();
+    const [y, x] = new TextDecoder().decode(data).match(/\[(\d+);(\d+)R/)?.slice(1, 3).map(Number) ?? [
+        0,
+        0, 
+    ];
+    return {
+        x,
+        y
+    };
+}
+export { mod2 as Ansi };
+export { mod3 as Keycode };
+export { getCursorPosition1 as getCursorPosition };
diff --git a/modules/esm/_repl/deno.js b/modules/esm/_repl/deno.js
new file mode 100644
index 00000000..51bf6c5a
--- /dev/null
+++ b/modules/esm/_repl/deno.js
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+// Cliffy uses Deno.build.os === 'windows' to substitute Windows-specific
+// ANSI codes.
+export const build = {
+    os: 'linux',
+};
+
+// We check for color support, ignore Deno's built-in check.
+export const noColor = false;
diff --git a/modules/esm/repl.js b/modules/esm/repl.js
new file mode 100644
index 00000000..1ca71806
--- /dev/null
+++ b/modules/esm/repl.js
@@ -0,0 +1,634 @@
+// 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 {Ansi, Keycode, Figures} from './_repl/cliffy.js';
+
+const Console = import.meta.importSync('_consoleNative');
+const FallbackConsole = import.meta.importSync('console');
+
+// 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}`;
+}
+
+const sInputHandler = Symbol('input handler');
+class ReplInput {
+    /**
+     * @param {object} _ _
+     * @param {Gio.UnixOutputStream} _.stdin the input stream to treat as stdin
+     * @param {Gio.UnixOutputStream} _.stdout the output stream to treat as stdout
+     * @param {Gio.UnixOutputStream} _.stderr the output stream to treat as stderr
+     * @param {boolean} _.enableColor whether to print ANSI color codes
+     * @param {string} _.prompt the prompt prefix
+     */
+    constructor({stdin, stdout, stderr, enableColor, prompt = 'gjs'}) {
+        this.stdin = stdin;
+        this.stdout = stdout;
+        this.stderr = stderr;
+
+        this._prompt = this._buildPrompt(prompt, enableColor);
+        this._cancelled = false;
+
+        /**
+         * Store previously inputted lines
+         *
+         * @type {string[]}
+         */
+        this.history = [];
+        /**
+         * Store pending lines
+         *
+         * @example
+         * gjs > 'a pending line...
+         * ..... '
+         *
+         * @type {string[]}
+         */
+        this.pendingInputLines = [];
+        /**
+         * The current input buffer (in chars)
+         *
+         * @type {string[]}
+         */
+        this.currentInputChars = [];
+
+        /**
+         * The cursor's current column position.
+         */
+        this.cursorColumn = 0;
+
+        this[sInputHandler] =
+            /**
+             * @param {string} _input the inputted line or lines (separated by \n)
+             */
+            _input => { };
+    }
+
+    /**
+     * @param {string} prompt a string to tag each new line with
+     * @param {boolean} enableColors whether to print with color
+     */
+    _buildPrompt(prompt, enableColors) {
+        const prefix = prompt;
+        const pointer = Figures.POINTER_SMALL;
+        const noColor = `${prefix} ${pointer} `;
+        const length = noColor.length;
+        const withColor =  `${Ansi.colors.yellow(prefix)} ${pointer} `;
+        const renderedPrompt = enableColors ? withColor : noColor;
+
+        return {
+            prefix,
+            pointer,
+            length,
+            renderedPrompt,
+        };
+    }
+
+    /**
+     * @returns {string}
+     */
+    getValue() {
+        if (this.historyIndex >= 0)
+            return this.history[this.historyIndex].join('');
+
+
+        return this.currentInputChars.join('');
+    }
+
+    /**
+     * @returns {string[]}
+     */
+    getEditableValue() {
+        if (this.historyIndex > -1) {
+            // TODO(ewlsh): This allows editing each history entry
+            // 'in place'.
+            return this.history[this.historyIndex];
+        }
+        return this.currentInputChars;
+    }
+
+    editValue(editor) {
+        if (this.historyIndex > -1) {
+            this.history[this.historyIndex] = editor(this.history[this.historyIndex]);
+            return this.history[this.historyIndex];
+        }
+
+        this.currentInputChars = editor(this.currentInputChars);
+        return this.currentInputChars;
+    }
+
+    validate(input) {
+        return Console.isValid(input);
+    }
+
+    clear(lines = 1) {
+        this.writeSync(Ansi.cursorLeft);
+        this.writeSync(Ansi.eraseDown(lines));
+    }
+
+    historyUp() {
+        if (this.historyIndex < this.history.length - 1) {
+            this.historyIndex++;
+            this.cursorColumn = -1;
+        }
+    }
+
+    historyDown() {
+        if (this.historyIndex >= 0) {
+            this.historyIndex--;
+            this.cursorColumn = -1;
+        }
+    }
+
+    moveCursorToBeginning() {
+        this.cursorColumn = 0;
+    }
+
+    moveCursorToEnd() {
+        this.cursorColumn = this.getValue().length;
+    }
+
+    moveCursorLeft() {
+        if (this.cursorColumn > 0)
+            this.cursorColumn--;
+    }
+
+    moveCursorRight() {
+        if (this.cursorColumn < this.getValue().length)
+            this.cursorColumn++;
+    }
+
+    addChar(char) {
+        const editableValue = this.getEditableValue();
+        editableValue.splice(this.cursorColumn, 0, char);
+
+        this.cursorColumn++;
+    }
+
+    deleteChar() {
+        const editableValue = this.getEditableValue();
+        if (this.cursorColumn > 0 && editableValue.length > 0)
+            editableValue.splice(this.cursorColumn - 1, 1);
+
+        this.moveCursorLeft();
+    }
+
+    deleteCharRightOrClose() {
+        const editableValue = this.getEditableValue();
+        if (this.cursorColumn < editableValue.length - 1)
+            editableValue.splice(this.cursorColumn, 1);
+        else
+            this.stop();
+    }
+
+    deleteToBeginning() {
+        const editableValue = this.getEditableValue();
+
+        editableValue.splice(0, this.cursorColumn);
+    }
+
+    deleteToEnd() {
+        const editableValue = this.getEditableValue();
+
+        editableValue.splice(this.cursorColumn);
+    }
+
+    /**
+     * Adapted from lib/readline.js in Node.js
+     */
+    _deleteWordLeft() {
+        this.editValue(value => {
+            if (this.cursorColumn > 0) {
+                // Reverse the string and match a word near beginning
+                // to avoid quadratic time complexity
+                let leading = value.slice(0, this.cursorColumn);
+                const reversed = [...leading].reverse().join('');
+                const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+                leading = leading.slice(0,
+                    leading.length - match[0].length);
+                value = leading.concat(value.slice(this.cursorColumn));
+                this.cursorColumn = leading.length;
+
+                return value;
+            }
+        });
+    }
+
+    /**
+     * Adapted from lib/readline.js in Node.js
+     */
+    _deleteWordRight() {
+        this.editValue(value => {
+            if (this.currentInputChars.length > 0 && this.cursorColumn < value.length) {
+                const trailing = value.slice(this.cursorColumn).join('');
+                const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
+                value = value.slice(0, this.cursorColumn).concat(
+                    trailing.slice(match[0].length));
+                return value;
+            }
+        });
+    }
+
+    /**
+     * Adapted from lib/readline.js in Node.js
+     */
+    _wordLeft() {
+        if (this.cursorColumn > 0) {
+            const value = this.getValue();
+            // Reverse the string and match a word near beginning
+            // to avoid quadratic time complexity
+            const leading = value.slice(0, this.cursorColumn);
+            const reversed = Array.from(leading).reverse().join('');
+            const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+
+            this.cursorColumn -= match[0].length;
+            this.cursorColumn = Math.max(0, this.cursorColumn);
+        }
+    }
+
+    /**
+     * Adapted from lib/readline.js in Node.js
+     */
+    _wordRight() {
+        const value = this.getValue();
+
+        if (this.cursorColumn < value.length) {
+            const trailing = value.slice(this.cursorColumn);
+            const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
+
+            this.cursorColumn += match[0].length;
+        }
+    }
+
+    processLine() {
+        const value = this.getValue();
+        // Rebuild the input...
+        const js = [...this.pendingInputLines, value].join('\n');
+
+        // Reset state...
+        this.history.unshift(value.split(''));
+        this.historyIndex = -1;
+        this.currentInputChars = [];
+        this.cursorColumn = 0;
+
+        // Append the new line...
+        this.writeSync('\n');
+        this.flush();
+
+        // Only trigger input if this is a compilable unit...
+        if (this.validate(js)) {
+            // Reset lines before input is triggered
+            this.pendingInputLines = [];
+            this[sInputHandler]?.(js);
+        } else {
+            // Buffer the input until a compilable unit is found...
+            this.pendingInputLines.push(value);
+        }
+    }
+
+    handleEvent(key) {
+        if (key.ctrl && !key.meta && !key.shift) {
+            switch (key.name) {
+            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':
+                // TODO: Consider using internal API instead...
+                console.clear();
+                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);
+    }
+
+    updateInputIndex(value) {
+        if (this.cursorColumn === -1)
+            this.cursorColumn = value.length;
+
+
+        // Ensure the input index isn't longer than the content...
+        this.cursorColumn = Math.min(this.cursorColumn, value.length);
+    }
+
+    render() {
+        const value = this.getValue();
+
+        // Prevent the cursor from flashing while we render...
+        this.writeSync(Ansi.cursorHide);
+        this.clear();
+
+        if (this.pendingInputLines.length > 0) {
+            // Create a prefix like '... ' that matches the current
+            // prompt length...
+            const prefix = ' '.padStart(this._prompt.length, '.');
+            this.writeSync(prefix + value);
+        } else {
+            this.writeSync(this._prompt.renderedPrompt + value);
+        }
+
+        this.updateInputIndex(value);
+        this.writeSync(Ansi.cursorTo(this._prompt.length + this.cursorColumn + 1));
+        this.writeSync(Ansi.cursorShow);
+        this.flush();
+    }
+
+    flush() {
+        this.stdout.flush(null);
+    }
+
+    /**
+     * @param {Uint8Array} buffer a string or Uint8Array to write to stdout
+     */
+    writeSync(buffer) {
+        if (typeof buffer === 'string')
+            buffer = new TextEncoder().encode(buffer);
+        this.stdout.write_bytes(buffer, null);
+    }
+
+    _readHandler(stream, result) {
+        this.cancellable = null;
+
+        if (this._cancelled)
+            return;
+
+        if (result) {
+            const gbytes = stream.read_bytes_finish(result);
+
+            const bytes = gbytes.toArray();
+            if (bytes.length > 0) {
+                for (const event of Keycode.parse(bytes)) {
+                    this.handleEvent(event);
+
+                    if (this._cancelled)
+                        break;
+
+                    this.render();
+                }
+            }
+        }
+
+        this.cancellable = new Gio.Cancellable();
+        stream.read_bytes_async(8, 0, this.cancellable, this._readHandler.bind(this));
+    }
+
+    cancel() {
+        this._cancelled = true;
+        this.cancellable?.cancel();
+        this.cancellable = null;
+
+        this.writeSync('\n');
+        this.flush();
+    }
+
+    read() {
+        this.render();
+
+        // Start the async read loop...
+        this._readHandler(this.stdin);
+    }
+
+    /**
+     *
+     * @param {(input: string) => void} inputHandler a callback when new lines are processed
+     */
+    prompt(inputHandler) {
+        this[sInputHandler] = inputHandler;
+        this._cancelled = false;
+        this.read();
+    }
+}
+
+const sCheckEnvironment = Symbol('check environment');
+const sSupportsColor = Symbol('supports color');
+const sMainLoop = Symbol('main loop');
+
+export class Repl {
+    constructor() {
+        ({Gio} = imports.gi);
+
+        this.lineNumber = 0;
+
+        Object.defineProperties(this, {
+            [sCheckEnvironment]: {
+                writable: true,
+                enumerable: false,
+                configurable: true,
+                value: this[sCheckEnvironment].bind(this),
+            },
+            [sSupportsColor]: {
+                writable: true,
+                enumerable: false,
+                configurable: true,
+                value: true,
+            },
+            [sMainLoop]: {
+                writable: true,
+                enumerable: false,
+                configurable: true,
+                value: null,
+            },
+        });
+    }
+
+    [sCheckEnvironment]() {
+        let hasUnixStreams = 'UnixInputStream' in Gio;
+        hasUnixStreams = hasUnixStreams && 'UnixOutputStream' in Gio;
+
+        if (!hasUnixStreams)
+            return false;
+
+        this[sSupportsColor] = GLib.log_writer_supports_color(1);
+        this[sSupportsColor] = this[sSupportsColor] && GLib.getenv('NO_COLOR') === null;
+        return true;
+    }
+
+    evaluate(lines) {
+        this.lineNumber++;
+
+        try {
+            const result = Console.eval(lines, this.lineNumber);
+
+            if (result !== undefined) {
+                const encoded = new TextEncoder().encode(`${toString(result)}\n`);
+                this.input.writeSync(encoded);
+                this.input.flush();
+            }
+        } catch (error) {
+            console.error(error);
+        }
+    }
+
+    run() {
+        if (!this[sCheckEnvironment]()) {
+            FallbackConsole.interact();
+            return;
+        }
+
+        const stdin = Gio.UnixInputStream.new(0, false);
+        const stdout = Gio.UnixOutputStream.new(1, false);
+        const stderr = Gio.UnixOutputStream.new(2, false);
+
+        this.input = new ReplInput({
+            stdin,
+            stdout,
+            stderr,
+            enableColor: this[sSupportsColor],
+        });
+
+        try {
+            Console.enableRawMode();
+
+            // Start accepting input and rendering...
+            this.input.prompt(lines => {
+                if (lines.trim().startsWith('exit()'))
+                    this.exit();
+                else
+                    this.evaluate(lines, stdout);
+            });
+
+            // Install our default mainloop...
+            this.replaceMainLoop(() => {
+                imports.mainloop.run('repl');
+            }, () => {
+                imports.mainloop.quit('repl');
+            });
+
+            let mainloop = this[sMainLoop];
+            while (mainloop) {
+                const [start] = mainloop;
+
+                start();
+
+                mainloop = this[sMainLoop];
+            }
+        } finally {
+            Console.disableRawMode();
+        }
+    }
+
+    exit() {
+        this.input.cancel();
+
+        const mainloop = this[sMainLoop];
+        this[sMainLoop] = null;
+
+        if (mainloop) {
+            const [, quit] = mainloop;
+
+            quit?.();
+        }
+    }
+
+    replaceMainLoop(start, quit = () => {
+        // Force an exit if a user doesn't define their
+        // replacement mainloop's quit function.
+        imports.system.exit(1);
+    }) {
+        const mainloop = this[sMainLoop];
+        this[sMainLoop] = [start, quit];
+
+        if (mainloop) {
+            const [, previousQuit] = mainloop;
+
+            previousQuit?.();
+        }
+    }
+}
diff --git a/modules/modules.cpp b/modules/modules.cpp
index c9b7061d..5ba66f98 100644
--- a/modules/modules.cpp
+++ b/modules/modules.cpp
@@ -22,5 +22,7 @@ gjs_register_static_modules (void)
 #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/test/check-pch.sh b/test/check-pch.sh
index fd27b10f..606502fb 100755
--- a/test/check-pch.sh
+++ b/test/check-pch.sh
@@ -122,6 +122,7 @@ INCLUDED_FILES=(
     \*.c
     \*.cpp
     \*.h
+    \*.hxx
 )
 
 grep_include_lines() {
diff --git a/tools/cliffy/.eslintrc.yml b/tools/cliffy/.eslintrc.yml
new file mode 100644
index 00000000..84e48aef
--- /dev/null
+++ b/tools/cliffy/.eslintrc.yml
@@ -0,0 +1,7 @@
+---
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+extends: '../../.eslintrc.yml'
+parserOptions:
+  sourceType: 'module'
+  ecmaVersion: 2022
diff --git a/tools/cliffy/ansi.js b/tools/cliffy/ansi.js
new file mode 100644
index 00000000..c7cf0c8c
--- /dev/null
+++ b/tools/cliffy/ansi.js
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+export * from 'https://deno.land/x/cliffy@v0.19.5/ansi/ansi.ts';
+export * from 'https://deno.land/x/cliffy@v0.19.5/ansi/ansi_escapes.ts';
+export * from 'https://deno.land/x/cliffy@v0.19.5/ansi/chain.ts';
+export * from 'https://deno.land/x/cliffy@v0.19.5/ansi/colors.ts';
+export * from 'https://deno.land/x/cliffy@v0.19.5/ansi/cursor_position.ts';
diff --git a/tools/cliffy/bundle.sh b/tools/cliffy/bundle.sh
new file mode 100755
index 00000000..c98e049e
--- /dev/null
+++ b/tools/cliffy/bundle.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+deno bundle --unstable ./tools/cliffy/lib.js ./modules/esm/_repl/cliffy.js
+gjs -m ./tools/cliffy/transform.js ./modules/esm/_repl/cliffy.js
diff --git a/tools/cliffy/lib.js b/tools/cliffy/lib.js
new file mode 100644
index 00000000..f0e27bfb
--- /dev/null
+++ b/tools/cliffy/lib.js
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: 2020 Benjamin Fischer <c4spar gmx de>
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+export * as Ansi from './ansi.js';
+
+export * as Keycode from 'https://deno.land/x/cliffy@v0.19.5/keycode/mod.ts';
+export {Figures} from 'https://deno.land/x/cliffy@v0.19.5/prompt/figures.ts';
+
+import {cursorPosition} from './ansi.js';
+
+/**
+ * Ported from Cliffy, originally
+ * depended on Deno.stdin/Deno.stdout
+ *
+ * @param {object} _ _
+ * @param {Gio.UnixInputStream} _.stdin the input stream for stdin
+ * @param {Gio.UnixOutputStream} _.stdout the output stream for stdout
+ */
+export function getCursorPosition({stdin, stdout}) {
+    stdout.write_bytes(new TextEncoder().encode(cursorPosition), null);
+    const data = stdin.read_bytes(8, null).toArray();
+    const [y, x] = new TextDecoder().decode(data).match(/\[(\d+);(\d+)R/)?.slice(1, 3).map(Number) ?? [
+        0,
+        0,
+    ];
+    return {
+        x,
+        y,
+    };
+}
diff --git a/tools/cliffy/transform.js b/tools/cliffy/transform.js
new file mode 100644
index 00000000..6aefb7f9
--- /dev/null
+++ b/tools/cliffy/transform.js
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+import System from 'system';
+import Gio from 'gi://Gio';
+
+const file = Gio.File.new_for_commandline_arg(System.programArgs[0]);
+const [, contents] = file.load_contents(null);
+
+let source = new TextDecoder().decode(contents);
+// Remove all private variable names until mozjs91 is supported.
+source = source.replace(/#/g, '$');
+source =
+    `// SPDX-License-Identifier: MIT
+// SPDX-FileCopyrightText: 2020 Benjamin Fischer <c4spar gmx de>
+
+// Inject our shim for the Deno global...
+import * as Deno from './deno.js';
+let globalThis = { Deno };
+${source}`;
+
+file.replace_contents(new TextEncoder().encode(source), null, false, 0, null);
+
+print('modules/esm/_repl/cliffy.js generated.');
diff --git a/tools/run_iwyu.sh b/tools/run_iwyu.sh
index 319da33e..458578f0 100755
--- a/tools/run_iwyu.sh
+++ b/tools/run_iwyu.sh
@@ -28,6 +28,7 @@ should_analyze () {
         all) return 0 ;;
         *$file*) return 0 ;;
         *${file%.cpp}.h*) return 0 ;;
+        *${file%.cpp}.hxx*) return 0 ;;
         *${file%.cpp}-inl.h*) return 0 ;;
         *${file%.cpp}-private.h*) return 0 ;;
         *${file%.c}.h*) return 0 ;;
diff --git a/util/console.cpp b/util/console.cpp
index 5489ab6b..4498d165 100644
--- a/util/console.cpp
+++ b/util/console.cpp
@@ -11,9 +11,14 @@
 #    include <io.h>
 #endif
 
+#ifdef HAVE_TERMIOS_H
+#    include <termios.h>
+#endif
+
 #include <glib.h>
 
 #include "util/console.h"
+#include "util/console.hxx"
 
 /**
  * ANSI escape code sequences to manipulate terminals.
@@ -32,6 +37,9 @@ constexpr const char CLEAR_SCREEN[] = "\x1b[2J";
 
 }  // namespace ANSICode
 
+namespace Gjs {
+namespace Console {
+
 #ifdef HAVE_UNISTD_H
 const int stdin_fd = STDIN_FILENO;
 const int stdout_fd = STDOUT_FILENO;
@@ -46,7 +54,63 @@ const int stdout_fd = 1;
 const int stderr_fd = 2;
 #endif
 
-bool gjs_console_is_tty(int fd) {
+#ifdef HAVE_TERMIOS_H
+struct termios saved_termios;
+#endif
+
+bool disable_raw_mode() {
+#ifdef HAVE_TERMIOS_H
+    return tcsetattr(stdin_fd, TCSAFLUSH, &saved_termios) != -1;
+#else
+    return false;
+#endif
+}
+
+void _disable_raw_mode() {
+    void* _ [[maybe_unused]] = reinterpret_cast<void*>(disable_raw_mode());
+}
+
+bool enable_raw_mode() {
+#ifdef HAVE_TERMIOS_H
+    // Save the current terminal flags to reset later
+    if (tcgetattr(stdin_fd, &saved_termios) == -1) {
+        if (disable_raw_mode())
+            return false;
+
+        return false;
+    }
+
+    // Register an exit handler to restore
+    // the terminal modes on exit.
+    atexit(_disable_raw_mode);
+
+    struct termios raw = saved_termios;
+    // - Disable \r to \n conversion on input
+    // - Disable parity checking
+    // - Disable stripping characters to 7-bits
+    // - Disable START/STOP characters
+    // https://www.gnu.org/software/libc/manual/html_node/Input-Modes.html
+    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+    // Enforce 8-bit characters
+    // https://www.gnu.org/software/libc/manual/html_node/Control-Modes.html
+    raw.c_cflag |= (CS8);
+    // Disable echoing (terminal reprinting input)
+    // Disable canonical mode (output reflects input)
+    // Disable "extensions" that allow users to inject
+    // https://www.gnu.org/software/libc/manual/html_node/Other-Special.html
+    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);
+    // Set 0 characters required for a read
+    raw.c_cc[VMIN] = 0;
+    // Set the read timeout to 1 decisecond (0.1 seconds)
+    raw.c_cc[VTIME] = 1;
+
+    return tcsetattr(stdin_fd, TCSAFLUSH, &raw) != -1;
+#else
+    return false;
+#endif
+}
+
+bool is_tty(int fd) {
 #ifdef HAVE_UNISTD_H
     return isatty(fd);
 #elif defined(_WIN32)
@@ -56,9 +120,22 @@ bool gjs_console_is_tty(int fd) {
 #endif
 }
 
-bool gjs_console_clear() {
+bool clear() {
     if (stdout_fd < 0 || !g_log_writer_supports_color(stdout_fd))
         return false;
 
     return fputs(ANSICode::CLEAR_SCREEN, stdout) > 0 && fflush(stdout) > 0;
 }
+
+}  // namespace Console
+}  // namespace Gjs
+
+// C compatibility definitions...
+
+const int stdin_fd = Gjs::Console::stdin_fd;
+const int stdout_fd = Gjs::Console::stdout_fd;
+const int stderr_fd = Gjs::Console::stderr_fd;
+
+bool gjs_console_is_tty(int fd) { return Gjs::Console::is_tty(fd); }
+
+bool gjs_console_clear() { return Gjs::Console::clear(); }
diff --git a/util/console.hxx b/util/console.hxx
new file mode 100644
index 00000000..70c0077e
--- /dev/null
+++ b/util/console.hxx
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#ifndef UTIL_CONSOLE_HXX_
+#define UTIL_CONSOLE_HXX_
+
+namespace Gjs {
+namespace Console {
+extern const int stdout_fd;
+extern const int stdin_fd;
+extern const int stderr_fd;
+
+[[nodiscard]] bool is_tty(int fd = stdout_fd);
+
+[[nodiscard]] bool clear();
+
+[[nodiscard]] bool enable_raw_mode();
+
+[[nodiscard]] bool disable_raw_mode();
+
+[[nodiscard]] bool get_size(int* width, int* height);
+
+};  // namespace Console
+};  // namespace Gjs
+
+
+#endif  // UTIL_CONSOLE_HXX_


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]