[gjs/ewlsh/nova-repl] Implement REPL with non-blocking mainloop




commit f4f77419b84869f42948e17a51d542fbffdafe39
Author: Evan Welsh <contact evanwelsh com>
Date:   Sat Aug 28 13:04:42 2021 -0700

    Implement REPL with non-blocking mainloop
    
    Closes #76

 js.gresource.xml    |   1 +
 meson.build         |   2 +
 modules/console.cpp |  98 +++++++++++--
 modules/console.h   |   4 +
 modules/esm/repl.js | 402 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 modules/modules.cpp |   2 +
 newConsole.js       |   7 +
 util/console.cpp    | 106 +++++++++++++-
 util/console.hxx    |  30 ++++
 9 files changed, 641 insertions(+), 11 deletions(-)
---
diff --git a/js.gresource.xml b/js.gresource.xml
index a730f2b8..46515458 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -18,6 +18,7 @@
     <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..a43accbb 100644
--- a/meson.build
+++ b/meson.build
@@ -305,7 +305,9 @@ if build_readline
             required: readline.found()))
 endif
 header_conf.set('USE_UNITY_BUILD', get_option('unity'))
+header_conf.set('HAVE_SYS_IOCTL_H', cxx.check_header('sys/ioctl.h'))
 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))
diff --git a/modules/console.cpp b/modules/console.cpp
index 2ef511aa..a437f573 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;
@@ -148,13 +150,9 @@ struct AutoCatchCtrlC {};
     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))
@@ -163,8 +161,8 @@ struct AutoCatchCtrlC {};
     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;
     }
@@ -172,6 +170,22 @@ struct AutoCatchCtrlC {};
     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;
 
@@ -276,6 +290,58 @@ 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) {
+    if (!Gjs::Console::enable_raw_mode()) {
+        gjs_throw(cx, "Unable to enable raw mode.");
+        return false;
+    }
+
+    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;
+}
+
+bool gjs_console_get_terminal_size(JSContext* cx, unsigned argc,
+                                   JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+    if (!obj)
+        return false;
+
+    int width, height;
+    if (!Gjs::Console::get_size(&width, &height) || width < 0 || height < 0) {
+        gjs_throw(cx, "Unable to retrieve terminal size for current output.\n");
+        return false;
+    }
+
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+    if (!JS_DefinePropertyById(cx, obj, atoms.height(), height,
+                               JSPROP_READONLY) ||
+        !JS_DefinePropertyById(cx, obj, atoms.width(), width, JSPROP_READONLY))
+        return false;
+
+    args.rval().setObject(*obj);
+    return true;
+}
+
 bool
 gjs_define_console_stuff(JSContext              *context,
                          JS::MutableHandleObject module)
@@ -286,3 +352,17 @@ gjs_define_console_stuff(JSContext              *context,
                                  gjs_console_interact, 1,
                                  GJS_MODULE_PROP_FLAGS);
 }
+
+bool gjs_define_console_private_stuff(JSContext* context,
+                                      JS::MutableHandleObject module) {
+    module.set(JS_NewPlainObject(context));
+
+    return JS_DefineFunction(context, module, "enable_raw_mode",
+                             gjs_console_enable_raw_mode, 0,
+                             GJS_MODULE_PROP_FLAGS) &&
+           JS_DefineFunction(context, module, "eval", gjs_console_eval_js, 0,
+                             GJS_MODULE_PROP_FLAGS) &&
+           JS_DefineFunction(context, module, "get_size",
+                             gjs_console_get_terminal_size, 0,
+                             GJS_MODULE_PROP_FLAGS);
+}
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/repl.js b/modules/esm/repl.js
new file mode 100644
index 00000000..3cd0a828
--- /dev/null
+++ b/modules/esm/repl.js
@@ -0,0 +1,402 @@
+import Gio from 'gi://Gio';
+
+const Console = import.meta.importSync('_consoleNative');
+
+/**
+ * @type {(letter: string) => number}
+ */
+const codeOf = (letter) => letter.charCodeAt(0);
+
+function toString(value) {
+    if (typeof value === 'function') {
+        return value.toString();
+    }
+    if (typeof value === 'object') {
+        try {
+            return JSON.stringify(value);
+        } catch {
+        }
+    }
+    return `${value}`;
+}
+
+/**
+ * @enum {string}
+ */
+const KeyCodeStrings = {
+    /**
+     * Enter
+     */
+    ENTER: '\x0d',
+    /**
+     * \n (newline)
+     */
+    LF: '\n',
+    /**
+     * Escape
+     */
+    ESCAPE: '\x1b',
+    /**
+     * Delete
+     */
+    DELETE: '\x7f',
+    /**
+     * Backspace
+     */
+    BACKSPACE: '\x08',
+    /**
+     * [
+     */
+    BRACKET_LEFT: '[',
+};
+
+/**
+ * @enum {number}
+ */
+const KeyCode = {
+    /**
+     * Enter
+     */
+    ENTER: 13,
+    /**
+     * \n (newline)
+     */
+    LF: codeOf('\n'),
+    /**
+     * Escape
+     */
+    ESCAPE: 27,
+    /**
+     * Delete
+     */
+    DELETE: 127,
+    /**
+     * Backspace
+     */
+    BACKSPACE: 8,
+    /**
+     * [
+     */
+    BRACKET_LEFT: codeOf('['),
+};
+
+const CSI = `${KeyCodeStrings.ESCAPE}[`;
+
+/**
+ * @enum {string}
+ */
+const ANSICodeStrings = {
+    ARROW_UP: 'A',
+    ARROW_DOWN: 'B',
+    CURSOR_FORWARD: 'C',
+    CURSOR_BACKWARD: 'D',
+    CURSOR_MOVE_ABSOLUTE: 'H',
+};
+
+/**
+ * @enum {number}
+ */
+const ANSICodes = {
+    ARROW_UP: codeOf(ANSICodeStrings.ARROW_UP),
+    ARROW_DOWN: codeOf(ANSICodeStrings.ARROW_DOWN),
+    CURSOR_FORWARD: codeOf(ANSICodeStrings.CURSOR_FORWARD),
+    CURSOR_BACKWARD: codeOf(ANSICodeStrings.CURSOR_BACKWARD),
+};
+
+export class Repl {
+    constructor(prompt = 'gjs>') {
+        if (!('UnixInputStream' in Gio))
+            throw new Error('Repl can only be used on systems with Gio Unix');
+
+        const stdin = Gio.UnixInputStream.new(0, false);
+        const stdout = Gio.UnixOutputStream.new(1, false);
+
+        this._stdin = stdin;
+        this._stdout = stdout;
+
+        this._mainloop = null;
+
+        this._keyBuffer = [];
+        this._commandHistory = [];
+        this._commandHistoryIndex = -1;
+        this._cursorColumn = 0;
+        this._escapeSequence = 0;
+
+        this.prompt = `${prompt} `;
+    }
+
+    get size() {
+        const { width, height } = Console.get_size();
+        return {
+            rows: height,
+            columns: width,
+        };
+    }
+
+    toHistory(index) {
+        let newcmd = this._commandHistory[index];
+        if (newcmd) {
+            this._keyBuffer = [...new TextEncoder().encode(newcmd)];
+
+            // Snap to the new length
+            this._cursorColumn = Math.min(newcmd.length, this._cursorColumn);
+        }
+
+        this.writeCurrentLine(true);
+    }
+
+    historyUp() {
+        if (this._commandHistory.length === 0) return;
+
+        if (this._commandHistoryIndex === -1) {
+            this._savedKeyBuffer = this._keyBuffer;
+        }
+
+        if (this._commandHistoryIndex < this._commandHistory.length - 1)
+            this._commandHistoryIndex++;
+
+        this.toHistory(this._commandHistoryIndex);
+    }
+
+    historyDown() {
+        if (this._commandHistoryIndex < 0) return;
+
+        // If we're at the last command, reset to the saved buffer
+        if (this._commandHistoryIndex == 0) {
+            // Only reset if a saved buffer exists
+            // (can be cleared by running a command from history)
+            if (this._savedKeyBuffer) {
+                this._keyBuffer = this._savedKeyBuffer;
+                this._savedKeyBuffer = null;
+                this._commandHistoryIndex = -1;
+
+                this.writeCurrentLine(true);
+            }
+
+            return;
+        }
+
+        this._commandHistoryIndex--;
+        this.toHistory(this._commandHistoryIndex);
+    }
+
+    cursorForward() {
+        if (this._cursorColumn < this._keyBuffer.length) {
+            this._cursorColumn++;
+            this.writeANSI(ANSICodeStrings.CURSOR_FORWARD);
+        }
+    }
+
+    cursorBackward() {
+        if (this._cursorColumn > 0) {
+            this._cursorColumn--;
+            this.writeANSI(ANSICodeStrings.CURSOR_BACKWARD);
+        }
+    }
+
+    delete() {
+        if (this._cursorColumn > 0) {
+            this._keyBuffer.splice(this._cursorColumn - 1, 1);
+            this._cursorColumn--;
+        }
+
+        this.writeCurrentLine();
+    }
+
+    dumpCommandBuffer() {
+        const cmd = new TextDecoder().decode(new Uint8Array(this._keyBuffer));
+
+        this._keyBuffer = [];
+        this._savedKeyBuffer = null;
+        this._cursorColumn = 0;
+
+        return cmd;
+    }
+
+    addHistory(cmd) {
+        this._commandHistory.unshift(cmd);
+        // Reset the history index
+        this._commandHistoryIndex = -1;
+    }
+
+    keypress(code) {
+        if (this._escapeSequence == 1) {
+            if (code === KeyCode.BRACKET_LEFT) {
+                this._escapeSequence = 2;
+                return;
+            } else {
+                this._escapeSequence = 0;
+            }
+        }
+
+        if (this._escapeSequence == 2) {
+            switch (code) {
+                case ANSICodes.ARROW_UP:
+                    this.historyUp();
+                    break;
+                case ANSICodes.ARROW_DOWN:
+                    this.historyDown();
+                    break;
+                case ANSICodes.CURSOR_FORWARD:
+                    this.cursorForward();
+                    break;
+                case ANSICodes.CURSOR_BACKWARD:
+                    this.cursorBackward();
+                    break;
+                default:
+                    this.write(KeyCodeStrings.ESCAPE);
+            }
+
+            this._escapeSequence = 0;
+
+            return;
+        }
+        if (code === KeyCode.BACKSPACE || code === KeyCode.DELETE) {
+            this.delete();
+        } else if (code === KeyCode.ESCAPE) {
+            // Start escape sequence...
+            this._escapeSequence = 1;
+        } else if (code === KeyCode.ENTER || code === KeyCode.LF) {
+            const cmd = this.dumpCommandBuffer();
+
+            // TODO: Add a hardcoded exit() for now.
+            if (cmd.startsWith('exit()')) {
+                this.stop();
+                this.writeLine();
+                return;
+            }
+
+            // Add the command to history before evaluation, in case it errors...
+            this.addHistory(cmd);
+            // Append a new line for the command's output...
+            this.writeLine();
+
+            try {
+                const result = Console.eval(cmd, this._commandHistory.length + 1);
+
+                if (result) {
+                    // TODO: Integrate with good printer...
+                    this.writeLine(`${toString(result)}`, false);
+                    this.write(this.prompt);
+                } else {
+                    this.writeLine(`'undefined'`);
+                    this.write(this.prompt);
+                }
+            } catch (err) {
+                console.error(err);
+            }
+        } else {
+            this._keyBuffer.splice(this._cursorColumn, 0, code);
+            this._cursorColumn++;
+
+            this.writeCurrentLine();
+        }
+    }
+
+    /**
+     * @param {string} code 
+     * @param {...number} [counts]
+     */
+    writeANSI(code, ...counts) {
+        this.write(`${CSI}${counts.join(';')}${code}`)
+    }
+
+    writeCurrentLine(reset = false) {
+        const { rows } = this.size;
+        // TODO: Convert this to the ANSI enums
+        // Reset cursor, clear entire line...
+        this.write(`\x1b[2K\x1b[1G${this.prompt}`, false);
+
+        if (this._keyBuffer.length > 0) {
+            const output = String.fromCharCode(...this._keyBuffer);
+            this.write(output, false);
+
+            if (reset) {
+                this._cursorColumn = output.length;
+            }
+        } else if (reset) {
+            this._cursorColumn = 0;
+        }
+        const cursor_position = this._cursorColumn + this.prompt.length + 1;
+        this.writeANSI(ANSICodeStrings.CURSOR_MOVE_ABSOLUTE, rows + 1, cursor_position);
+    }
+
+    /**
+     * @param {string} output 
+     * @param {boolean} [flush] 
+     */
+    write(output, flush = true) {
+        const cancellable = new Gio.Cancellable();
+        const bytes = new TextEncoder().encode(output);
+        this._stdout.write_bytes(bytes, cancellable);
+        if (flush)
+            this._stdout.flush(cancellable);
+    }
+
+    /**
+     * @param {string} output 
+     * @param {boolean} [flush] 
+     */
+    writeLine(output = '', flush = true) {
+        this.write(`${output}${KeyCodeStrings.LF}`, flush);
+    }
+
+    read(stdin, result) {
+        if (result) {
+            const gbytes = this._stdin.read_bytes_finish(result);
+            const bytes = gbytes.toArray();
+            if (bytes.length > 0) {
+                for (const byte of bytes) {
+                    this.keypress(byte);
+                }
+            }
+        }
+
+        const cancellable = new Gio.Cancellable();
+        stdin.read_bytes_async(1, 0, cancellable, this.read.bind(this));
+    }
+
+    run() {
+        Console.enable_raw_mode();
+
+        this.write(`${this.prompt}`);
+        this.read(this._stdin);
+
+        this.replaceMainLoop(() => {
+            imports.mainloop.run('repl');
+        }, () => {
+            imports.mainloop.quit('repl');
+        });
+
+        let mainloop;
+        while (mainloop = this._mainloop) {
+            const [start] = mainloop;
+
+            start();
+        }
+    }
+
+    stop() {
+        const mainloop = this._mainloop;
+        this._mainloop = null;
+
+        if (mainloop) {
+            const [, quit] = mainloop;
+
+            quit?.();
+        }
+
+
+    }
+
+    replaceMainLoop(start, quit = () => { System.exit(1); }) {
+        const mainloop = this._mainloop;
+        this._mainloop = [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/newConsole.js b/newConsole.js
new file mode 100644
index 00000000..cd396c92
--- /dev/null
+++ b/newConsole.js
@@ -0,0 +1,7 @@
+import { Repl } from 'repl';
+
+const repl = new Repl();
+
+globalThis.repl = repl;
+
+repl.run();
diff --git a/util/console.cpp b/util/console.cpp
index 5489ab6b..f77ee738 100644
--- a/util/console.cpp
+++ b/util/console.cpp
@@ -4,6 +4,8 @@
 #include <config.h>
 
 #include <stdio.h>
+#include <stdexcept>
+#include <string>
 
 #ifdef HAVE_UNISTD_H
 #    include <unistd.h>
@@ -11,9 +13,22 @@
 #    include <io.h>
 #endif
 
+#ifdef HAVE_TERMIOS_H
+#    include <termios.h>
+#endif
+
+#if defined(HAVE_SYS_IOCTL_H)
+#    include <fcntl.h>
+#    include <sys/ioctl.h>
+#    if defined(TIOCGWINSZ)
+#        define GET_SIZE_USE_IOCTL
+#    endif
+#endif
+
 #include <glib.h>
 
 #include "util/console.h"
+#include "util/console.hxx"
 
 /**
  * ANSI escape code sequences to manipulate terminals.
@@ -32,6 +47,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 +64,48 @@ 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
+    if (tcgetattr(stdin_fd, &saved_termios) == -1) {
+        if (disable_raw_mode())
+            return false;
+
+        return false;
+    }
+
+    atexit(_disable_raw_mode);
+
+    struct termios raw = saved_termios;
+    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+    raw.c_cflag |= (CS8);
+    // TODO: Add ISIG to take over CTRL+C
+    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);
+    raw.c_cc[VMIN] = 0;
+    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 +115,52 @@ 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;
 }
+
+bool get_size(int* width, int* height) {
+    {
+        const char* lines = g_getenv("LINES");
+        const char* columns = g_getenv("COLUMNS");
+        try {
+            if (columns && lines) {
+                *width = std::stoi(columns);
+                *height = std::stoi(lines);
+                return true;
+            }
+        } catch (const std::invalid_argument& ia) {
+        } catch (const std::out_of_range& oor) {
+        }
+    }
+
+#ifdef GET_SIZE_USE_IOCTL
+    struct winsize ws;
+
+    if (ioctl(stdout_fd, TIOCGWINSZ, &ws) < 0) {
+        return false;
+    }
+
+    *width = ws.ws_col;
+    *height = ws.ws_row;
+    return true;
+#else
+    return false;
+#endif
+}
+
+}  // 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..db6b6342
--- /dev/null
+++ b/util/console.hxx
@@ -0,0 +1,30 @@
+/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * 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_H_


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