[gjs/ewlsh/nova-repl: 3/7] Implement non-blocking Repl with Mainloop
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/nova-repl: 3/7] Implement non-blocking Repl with Mainloop
- Date: Sat, 4 Sep 2021 23:55:24 +0000 (UTC)
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]