[gjs/ewlsh/nova-repl] Implement REPL with non-blocking mainloop
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/nova-repl] Implement REPL with non-blocking mainloop
- Date: Sat, 28 Aug 2021 20:12:29 +0000 (UTC)
commit 146967b6627b4839ecbae7efeab62339a274beab
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 | 102 +++++++++++--
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, 645 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..678a2786 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,62 @@ 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, "enable_raw_mode", args, ""))
+ return false;
+
+ 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 +356,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]