[gjs/ewlsh/nova-repl] Replace Console.interact with Repl-based API



commit 79d2d67509944144f18330d069204629a69f60d3
Author: Evan Welsh <contact evanwelsh com>
Date:   Fri Sep 3 11:50:23 2021 -0700

    Replace Console.interact with Repl-based API

 js.gresource.xml                  |   1 +
 modules/console.cpp               | 184 +++-----------------------------------
 modules/console.h                 |   4 -
 modules/esm/_bootstrap/default.js |   2 +
 modules/esm/repl.js               | 172 +++++++++++++++++++++++++----------
 modules/modules.cpp               |   1 -
 modules/script/console.js         |  12 +++
 7 files changed, 150 insertions(+), 226 deletions(-)
---
diff --git a/js.gresource.xml b/js.gresource.xml
index a5c057b0..efd07bc4 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -36,6 +36,7 @@
 
     <file>modules/script/byteArray.js</file>
     <file>modules/script/cairo.js</file>
+    <file>modules/script/console.js</file>
     <file>modules/script/gettext.js</file>
     <file>modules/script/lang.js</file>
     <file>modules/script/_legacy.js</file>
diff --git a/modules/console.cpp b/modules/console.cpp
index 4daa45ef..ccef2bc3 100644
--- a/modules/console.cpp
+++ b/modules/console.cpp
@@ -15,13 +15,6 @@
 #    endif
 #endif
 
-#ifdef HAVE_READLINE_READLINE_H
-#    include <stdio.h>  // include before readline/readline.h
-
-#    include <readline/history.h>
-#    include <readline/readline.h>
-#endif
-
 #include <string>
 
 #include <glib.h>
@@ -92,59 +85,14 @@ public:
     }
 };
 
-
-// Adapted from https://stackoverflow.com/a/17035073/172999
-class AutoCatchCtrlC {
-#ifdef HAVE_SIGNAL_H
-    void (*m_prev_handler)(int);
-
-    static void handler(int signal) {
-        if (signal == SIGINT)
-            siglongjmp(jump_buffer, 1);
-    }
-
- public:
-    static sigjmp_buf jump_buffer;
-
-    AutoCatchCtrlC() {
-        m_prev_handler = signal(SIGINT, &AutoCatchCtrlC::handler);
-    }
-
-    ~AutoCatchCtrlC() {
-        if (m_prev_handler != SIG_ERR)
-            signal(SIGINT, m_prev_handler);
-    }
-
-    void raise_default() {
-        if (m_prev_handler != SIG_ERR)
-            signal(SIGINT, m_prev_handler);
-        raise(SIGINT);
-    }
-#endif  // HAVE_SIGNAL_H
-};
-
-#ifdef HAVE_SIGNAL_H
-sigjmp_buf AutoCatchCtrlC::jump_buffer;
-#endif  // HAVE_SIGNAL_H
-
 [[nodiscard]] static bool gjs_console_readline(char** bufp,
                                                const char* prompt) {
-#ifdef HAVE_READLINE_READLINE_H
-    char *line;
-    line = readline(prompt);
-    if (!line)
-        return false;
-    if (line[0] != '\0')
-        add_history(line);
-    *bufp = line;
-#else   // !HAVE_READLINE_READLINE_H
     char line[256];
     fprintf(stdout, "%s", prompt);
     fflush(stdout);
     if (!fgets(line, sizeof line, stdin))
         return false;
     *bufp = g_strdup(line);
-#endif  // !HAVE_READLINE_READLINE_H
     return true;
 }
 
@@ -172,120 +120,22 @@ 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) {
-    JS::RootedValue result(cx);
-    if (!gjs_console_eval(cx, bytes, lineno, &result)) {
-        return false;
-    }
-
-    if (result.isUndefined())
-        return true;
-
-    g_fprintf(stdout, "%s\n", gjs_value_debug_string(cx, result).c_str());
-    return true;
-}
-
 GJS_JSAPI_RETURN_CONVENTION
-static bool
-gjs_console_interact(JSContext *context,
-                     unsigned   argc,
-                     JS::Value *vp)
-{
-    JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
-    bool eof, exit_warning;
+static bool gjs_console_interact(JSContext* context, unsigned argc,
+                                 JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
     JS::RootedObject global(context, gjs_get_import_global(context));
-    char* temp_buf;
-    int lineno;
-    int startline;
-
-#ifndef HAVE_READLINE_READLINE_H
-    int rl_end = 0;  // nonzero if using readline and any text is typed in
-#endif
 
-    JS::SetWarningReporter(context, gjs_console_warning_reporter);
-
-    AutoCatchCtrlC ctrl_c;
-
-    // Separate initialization from declaration because of possible overwriting
-    // when siglongjmp() jumps into this function
-    eof = exit_warning = false;
-    temp_buf = nullptr;
-    lineno = 1;
-    do {
-        /*
-         * Accumulate lines until we get a 'compilable unit' - one that either
-         * generates an error (before running out of source) or that compiles
-         * cleanly.  This should be whenever we get a complete statement that
-         * coincides with the end of a line.
-         */
-        startline = lineno;
-        std::string buffer;
-        do {
-#ifdef HAVE_SIGNAL_H
-            // sigsetjmp() returns 0 if control flow encounters it normally, and
-            // nonzero if it's been jumped to. In the latter case, use a while
-            // loop so that we call sigsetjmp() a second time to reinit the jump
-            // buffer.
-            while (sigsetjmp(AutoCatchCtrlC::jump_buffer, 1) != 0) {
-                g_fprintf(stdout, "\n");
-                if (buffer.empty() && rl_end == 0) {
-                    if (!exit_warning) {
-                        g_fprintf(stdout,
-                                  "(To exit, press Ctrl+C again or Ctrl+D)\n");
-                        exit_warning = true;
-                    } else {
-                        ctrl_c.raise_default();
-                    }
-                } else {
-                    exit_warning = false;
-                }
-                buffer.clear();
-                startline = lineno = 1;
-            }
-#endif  // HAVE_SIGNAL_H
-
-            if (!gjs_console_readline(
-                    &temp_buf, startline == lineno ? "gjs> " : ".... ")) {
-                eof = true;
-                break;
-            }
-            buffer += temp_buf;
-            buffer += "\n";
-            g_free(temp_buf);
-            lineno++;
-        } while (!JS_Utf8BufferIsCompilableUnit(context, global, buffer.c_str(),
-                                                buffer.size()));
-
-        bool ok;
-        {
-            AutoReportException are(context);
-            ok = gjs_console_eval_and_print(context, buffer, startline);
-        }
-        exit_warning = false;
-
-        GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
-        ok = gjs->run_jobs_fallible() && ok;
-
-        if (!ok) {
-            /* If this was an uncatchable exception, throw another uncatchable
-             * exception on up to the surrounding JS::Evaluate() in main(). This
-             * happens when you run gjs-console and type imports.system.exit(0);
-             * at the prompt. If we don't throw another uncatchable exception
-             * here, then it's swallowed and main() won't exit. */
-            return false;
-        }
-    } while (!eof);
+    JS::UniqueChars prompt;
+    if (!gjs_parse_call_args(context, "interact", args, "s", "prompt", &prompt))
+        return false;
 
-    g_fprintf(stdout, "\n");
+    GjsAutoChar buffer;
+    if (!gjs_console_readline(buffer.out(), prompt.get())) {
+        return true;
+    }
 
-    argv.rval().setUndefined();
-    return true;
+    return gjs_string_from_utf8(context, buffer, args.rval());
 }
 
 GJS_JSAPI_RETURN_CONVENTION
@@ -348,18 +198,8 @@ static bool gjs_console_is_valid_js(JSContext* cx, unsigned argc,
     return true;
 }
 
-bool
-gjs_define_console_stuff(JSContext              *context,
-                         JS::MutableHandleObject module)
-{
-    module.set(JS_NewPlainObject(context));
-    const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
-    return JS_DefineFunctionById(context, module, atoms.interact(),
-                                 gjs_console_interact, 1,
-                                 GJS_MODULE_PROP_FLAGS);
-}
-
 static JSFunctionSpec private_module_funcs[] = {
+    JS_FN("interact", gjs_console_interact, 1, GJS_MODULE_PROP_FLAGS),
     JS_FN("enableRawMode", gjs_console_enable_raw_mode, 0,
           GJS_MODULE_PROP_FLAGS),
     JS_FN("disableRawMode", gjs_console_disable_raw_mode, 0,
diff --git a/modules/console.h b/modules/console.h
index d63df22e..7df6a02d 100644
--- a/modules/console.h
+++ b/modules/console.h
@@ -11,10 +11,6 @@
 
 #include "gjs/macros.h"
 
-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);
diff --git a/modules/esm/_bootstrap/default.js b/modules/esm/_bootstrap/default.js
index afb155b0..242c38ef 100644
--- a/modules/esm/_bootstrap/default.js
+++ b/modules/esm/_bootstrap/default.js
@@ -7,3 +7,5 @@
 import '_encoding/encoding';
 // Bootstrap the Console API
 import 'console';
+// Install the Repl constructor for Console.interact()
+import 'repl';
diff --git a/modules/esm/repl.js b/modules/esm/repl.js
index 794d789e..648f0b66 100644
--- a/modules/esm/repl.js
+++ b/modules/esm/repl.js
@@ -9,7 +9,6 @@ import Gio from 'gi://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...
 /**
@@ -84,6 +83,10 @@ class ReplInput {
             _input => { };
     }
 
+    [Symbol.toStringTag]() {
+        return 'Repl';
+    }
+
     /**
      * @param {string} prompt a string to tag each new line with
      * @param {boolean} enableColors whether to print with color
@@ -93,7 +96,7 @@ class ReplInput {
         const pointer = Figures.POINTER_SMALL;
         const noColor = `${prefix} ${pointer} `;
         const length = noColor.length;
-        const withColor =  `${Ansi.colors.yellow(prefix)} ${pointer} `;
+        const withColor = `${Ansi.colors.yellow(prefix)} ${pointer} `;
         const renderedPrompt = enableColors ? withColor : noColor;
 
         return {
@@ -280,6 +283,10 @@ class ReplInput {
         }
     }
 
+    get isRaw() {
+        return true;
+    }
+
     processLine() {
         const value = this.getValue();
         // Rebuild the input...
@@ -291,9 +298,13 @@ class ReplInput {
         this.currentInputChars = [];
         this.cursorColumn = 0;
 
-        // Append the new line...
-        this.writeSync('\n');
-        this.flush();
+        // In raw mode we need to manually write a
+        // new line...
+        if (this.isRaw) {
+            // Append the new line...
+            this.writeSync('\n');
+            this.flush();
+        }
 
         // Only trigger input if this is a compilable unit...
         if (this.validate(js)) {
@@ -410,6 +421,16 @@ class ReplInput {
         this.cursorColumn = Math.min(this.cursorColumn, value.length);
     }
 
+    getPrompt() {
+        if (this.pendingInputLines.length > 0) {
+            // Create a prefix like '... ' that matches the current
+            // prompt length...
+            return ' '.padStart(this._prompt.length, '.');
+        }
+
+        return this._prompt.renderedPrompt;
+    }
+
     render() {
         const value = this.getValue();
 
@@ -417,14 +438,8 @@ class ReplInput {
         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);
-        }
+        const prompt = this.getPrompt();
+        this.writeSync(prompt + value);
 
         this.updateInputIndex(value);
         this.writeSync(Ansi.cursorTo(this._prompt.length + this.cursorColumn + 1));
@@ -445,7 +460,21 @@ class ReplInput {
         this.stdout.write_bytes(buffer, null);
     }
 
-    _readHandler(stream, result) {
+    handleInput(bytes) {
+        if (bytes.length === 0)
+            return;
+
+        for (const event of Keycode.parse(bytes)) {
+            this.handleEvent(event);
+
+            if (this._cancelled)
+                break;
+
+            this.render();
+        }
+    }
+
+    _asyncReadHandler(stream, result) {
         this.cancellable = null;
 
         if (this._cancelled)
@@ -454,21 +483,11 @@ class ReplInput {
         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.handleInput(gbytes.toArray());
         }
 
         this.cancellable = new Gio.Cancellable();
-        stream.read_bytes_async(8, 0, this.cancellable, this._readHandler.bind(this));
+        stream.read_bytes_async(8, 0, this.cancellable, this._asyncReadHandler.bind(this));
     }
 
     cancel() {
@@ -484,7 +503,7 @@ class ReplInput {
         this.render();
 
         // Start the async read loop...
-        this._readHandler(this.stdin);
+        this._asyncReadHandler(this.stdin);
     }
 
     /**
@@ -498,6 +517,35 @@ class ReplInput {
     }
 }
 
+class FallbackReplInput extends ReplInput {
+    constructor() {
+        super({});
+    }
+
+    read() {
+        while (!this._cancelled) {
+            const prompt = this.getPrompt();
+            this.editValue(() => {
+                return Console.interact(prompt).split('');
+            });
+
+            this.processLine();
+        }
+    }
+
+    get isRaw() {
+        return false;
+    }
+
+    writeSync(buffer) {
+        if (buffer instanceof Uint8Array)
+            buffer = new TextDecoder().decode(buffer);
+        print(buffer);
+    }
+
+    flush() { }
+}
+
 const sCheckEnvironment = Symbol('check environment');
 const sSupportsColor = Symbol('supports color');
 const sMainLoop = Symbol('main loop');
@@ -505,6 +553,7 @@ const sMainLoop = Symbol('main loop');
 export class Repl {
     constructor() {
         this.lineNumber = 0;
+        this.isRaw = false;
 
         Object.defineProperties(this, {
             [sCheckEnvironment]: {
@@ -529,14 +578,19 @@ export class Repl {
     }
 
     [sCheckEnvironment]() {
+        this[sSupportsColor] = GLib.log_writer_supports_color(1);
+        this[sSupportsColor] = this[sSupportsColor] && GLib.getenv('NO_COLOR') === null;
+
         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;
+        // TODO: Environment variable for testing.
+        if (GLib.getenv('GJS_REPL_NO_MAINLOOP'))
+            return false;
+
         return true;
     }
 
@@ -547,7 +601,9 @@ export class Repl {
             const result = Console.eval(lines, this.lineNumber);
 
             if (result !== undefined) {
-                const encoded = new TextEncoder().encode(`${toString(result)}\n`);
+                const encoded = new TextEncoder().encode(
+                    `${toString(result)}${this.isRaw ? '\n' : ''}`
+                );
                 this.input.writeSync(encoded);
                 this.input.flush();
             }
@@ -556,34 +612,47 @@ export class Repl {
         }
     }
 
+    _registerInputHandler() {
+        // Start accepting input and rendering...
+        this.input.prompt(lines => {
+            if (lines.trim().startsWith('exit()'))
+                this.exit();
+            else
+                this.evaluate(lines);
+        });
+    }
+
     run() {
         if (!this[sCheckEnvironment]()) {
-            FallbackConsole.interact();
+            this.input = new FallbackReplInput();
+
+            this._registerInputHandler();
             return;
         }
 
-        const stdin = Gio.UnixInputStream.new(0, false);
-        const stdout = Gio.UnixOutputStream.new(1, false);
-        const stderr = Gio.UnixOutputStream.new(2, false);
+        try {
+            this.isRaw = Console.enableRawMode();
 
-        this.input = new ReplInput({
-            stdin,
-            stdout,
-            stderr,
-            enableColor: this[sSupportsColor],
-        });
+            if (!this.isRaw) {
+                this.input = new FallbackReplInput();
 
-        try {
-            Console.enableRawMode();
-
-            // Start accepting input and rendering...
-            this.input.prompt(lines => {
-                if (lines.trim().startsWith('exit()'))
-                    this.exit();
-                else
-                    this.evaluate(lines, stdout);
+                this._registerInputHandler();
+                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],
             });
 
+            this._registerInputHandler();
+
             // Install our default mainloop...
             this.replaceMainLoop(() => {
                 imports.mainloop.run('repl');
@@ -622,6 +691,9 @@ export class Repl {
         // replacement mainloop's quit function.
         System.exit(1);
     }) {
+        if (!(this.input instanceof ReplInput))
+            return;
+
         const mainloop = this[sMainLoop];
         this[sMainLoop] = [start, quit];
 
@@ -632,3 +704,5 @@ export class Repl {
         }
     }
 }
+
+imports.console.Repl = Repl;
diff --git a/modules/modules.cpp b/modules/modules.cpp
index 5ba66f98..f0a950d8 100644
--- a/modules/modules.cpp
+++ b/modules/modules.cpp
@@ -21,7 +21,6 @@ gjs_register_static_modules (void)
     gjs_register_native_module("cairoNative", gjs_js_define_cairo_stuff);
 #endif
     gjs_register_native_module("system", gjs_js_define_system_stuff);
-    gjs_register_native_module("console", gjs_define_console_stuff);
     gjs_register_native_module("_consoleNative",
                                gjs_define_console_private_stuff);
     gjs_register_native_module("_print", gjs_define_print_stuff);
diff --git a/modules/script/console.js b/modules/script/console.js
new file mode 100644
index 00000000..82ae75cc
--- /dev/null
+++ b/modules/script/console.js
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+/* exported Repl, interact */
+
+var Repl = null;
+
+function interact() {
+    const repl = new Repl();
+
+    repl.run();
+}


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