[gjs/ewlsh/whatwg-console: 1/3] modules: Implement console.table()
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/whatwg-console: 1/3] modules: Implement console.table()
- Date: Sat, 7 Aug 2021 07:35:08 +0000 (UTC)
commit a57569915b3e295c16846ef5042464b6d76241ff
Author: Evan Welsh <contact evanwelsh com>
Date: Tue Jun 29 01:06:11 2021 -0700
modules: Implement console.table()
modules/console.cpp | 33 ++++++-
modules/esm/console.js | 238 ++++++++++++++++++++++++++++++++++++++++++++++++-
util/console.cpp | 65 ++++++++++++++
util/console.h | 1 +
4 files changed, 332 insertions(+), 5 deletions(-)
---
diff --git a/modules/console.cpp b/modules/console.cpp
index 70d0b7a1..b9f8d7de 100644
--- a/modules/console.cpp
+++ b/modules/console.cpp
@@ -17,7 +17,6 @@
#ifdef HAVE_READLINE_READLINE_H
# include <stdio.h> // include before readline/readline.h
-
# include <readline/history.h>
# include <readline/readline.h>
#endif
@@ -32,6 +31,7 @@
#include <js/CompileOptions.h>
#include <js/ErrorReport.h>
#include <js/Exception.h>
+#include <js/PropertyDescriptor.h>
#include <js/RootingAPI.h>
#include <js/SourceText.h>
#include <js/TypeDecls.h>
@@ -277,13 +277,42 @@ gjs_console_interact(JSContext *context,
return true;
}
+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;
+ Gjs::Console::size(&width, &height);
+
+ if (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)
{
module.set(JS_NewPlainObject(context));
const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
- return JS_DefineFunctionById(context, module, atoms.interact(),
+ return JS_DefineFunction(context, module, "getTerminalSize",
+ gjs_console_get_terminal_size, 1,
+ GJS_MODULE_PROP_FLAGS) &&
+ JS_DefineFunctionById(context, module, atoms.interact(),
gjs_console_interact, 1,
GJS_MODULE_PROP_FLAGS);
}
diff --git a/modules/esm/console.js b/modules/esm/console.js
index 1bbe98c3..6a9c6481 100644
--- a/modules/esm/console.js
+++ b/modules/esm/console.js
@@ -2,13 +2,71 @@
// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
// eslint-disable-next-line
-/// <reference lib='es2019' />
+/// <reference lib='es2020' />
// @ts-check
// @ts-expect-error
import GLib from 'gi://GLib';
+const {getTerminalSize: getNativeTerminalSize } =
+ // @ts-expect-error
+ import.meta.importSync('console');
+
+export { getNativeTerminalSize };
+
+/**
+ * @typedef TerminalSize
+ * @property {number} width
+ * @property {number} height
+ */
+
+/**
+ * Gets the terminal size from environment variables.
+ *
+ * @returns {TerminalSize | null}
+ */
+function getTerminalSizeFromEnvironment() {
+ const rawColumns = GLib.getenv('COLUMNS');
+ const rawLines = GLib.getenv('LINES');
+
+ if (rawColumns === null || rawLines === null)
+ return null;
+
+ const columns = Number.parseInt(rawColumns, 10);
+ const lines = Number.parseInt(rawLines, 10);
+
+ if (Number.isNaN(columns) || Number.isNaN(lines))
+ return null;
+
+ return {
+ width: columns,
+ height: lines,
+ };
+}
+
+/**
+ * @returns {TerminalSize}
+ */
+export function getTerminalSize() {
+ let size = getTerminalSizeFromEnvironment();
+ if (size)
+ return size;
+
+ try {
+ size = getNativeTerminalSize();
+ if (size)
+ return size;
+ } catch {}
+
+ // Return a default size if we can't determine the current terminal (if any)
+ // dimensions.
+ return {
+ width: 80,
+ height: 60,
+ };
+}
+
const sLogger = Symbol('Logger');
const sPrinter = Symbol('Printer');
const sFormatter = Symbol('Formatter');
@@ -140,8 +198,182 @@ export class Console {
}
// 1.1.7 table(tabularData, properties)
- table(_tabularData, _properties) {
- throw new Error('table() is not implemented.');
+ table(tabularData, properties) {
+ const COLUMN_PADDING = 1;
+ const SEPARATOR = '|';
+ const PLACEHOLDER = '…';
+
+ // If the data is not an object (and non-null) we can't log anything as
+ // a table.
+ if (typeof tabularData !== 'object' || tabularData === null)
+ return;
+
+ const rows = Object.keys(tabularData);
+ // If there are no rows, we can't log anything as a table.
+ if (rows.length === 0)
+ return;
+
+ // Get all possible columns from each row of data...
+ const objectColumns = rows
+ .filter(
+ key =>
+ tabularData[key] !== null &&
+ typeof tabularData[key] === 'object'
+ )
+ .map(key => Object.keys(tabularData[key]))
+ .flat();
+ // De-duplicate columns and sort alphabetically...
+ const objectColumnKeys = [...new Set(objectColumns)].sort();
+ // Determine if there are any rows which cannot be placed in columns
+ // (they aren't objects or arrays)
+ const hasNonColumnValues = rows.some(
+ key =>
+ tabularData[key] === null ||
+ typeof tabularData[key] !== 'object'
+ );
+
+ // Used as a placeholder for a catch-all Values column
+ const Values = Symbol('Values');
+
+ /** @type {any[]} */
+ let columns = objectColumnKeys;
+
+ if (Array.isArray(properties))
+ columns = [...properties];
+ else if (hasNonColumnValues)
+ columns = [...objectColumnKeys, Values];
+
+ const {width} = getTerminalSize();
+ const columnCount = columns.length;
+ const horizontalColumnPadding = COLUMN_PADDING * 2;
+ // Subtract n+2 separator lengths because there are 2 more separators
+ // than columns. The 2 extra bound the index column.
+ const dividableWidth = width - (columnCount + 2) * SEPARATOR.length;
+
+ const maximumIndexColumnWidth =
+ dividableWidth - columnCount * (horizontalColumnPadding * COLUMN_PADDING + 1);
+ const largestIndexColumnContentWidth = rows.reduce(
+ (prev, next) => Math.max(prev, next.length),
+ 0
+ );
+ // This is the width of the index column with *no* width constraint.
+ const optimalIndexColumnWidth =
+ largestIndexColumnContentWidth + horizontalColumnPadding;
+ // Constrain the column width by the terminal width...
+ const indexColumnWidth = Math.min(
+ maximumIndexColumnWidth,
+ optimalIndexColumnWidth
+ );
+ // Calculate the amount of space each data column can take up,
+ // given the index column...
+ const spacing = Math.floor(
+ (dividableWidth - indexColumnWidth) / columnCount
+ );
+
+ /**
+ * @param {string} content a string to format within a column
+ * @param {number} totalWidth the total width the column can take up, including padding
+ */
+ function formatColumn(content, totalWidth) {
+ const halfPadding = Math.ceil((totalWidth - content.length) / 2);
+
+ if (content.length > totalWidth - horizontalColumnPadding) {
+ // Subtract horizontal padding and placeholder length.
+ const truncatedCol = content.substr(
+ 0,
+ totalWidth - horizontalColumnPadding - PLACEHOLDER.length
+ );
+ const padding = ''.padStart(COLUMN_PADDING, ' ');
+
+ return `${padding}${truncatedCol}${PLACEHOLDER}${padding}`;
+ } else {
+ return `${content
+ // Pad start to half the intended length (-1 to account for padding)
+ .padStart(content.length + halfPadding, ' ')
+ // Pad end to entire width
+ .padEnd(totalWidth, ' ')}`;
+ }
+ }
+
+ /**
+ *
+ */
+ function formatRow(indexCol, cols, separator = '|') {
+ return `${separator}${[indexCol, ...cols].join(
+ separator
+ )}${separator}`;
+ }
+
+ // Like +----+----+
+ const borderLine = formatRow(
+ '---'.padStart(indexColumnWidth, '-'),
+ columns.map(() => '---'.padStart(spacing, '-')),
+ '+'
+ );
+
+ /**
+ * @param {unknown} val a value to format into a string representation
+ * @returns {string}
+ */
+ function formatValue(val) {
+ let output;
+ if (typeof val === 'string')
+ output = val;
+ else if (
+ Array.isArray(val) ||
+ String(val) === '[object Object]'
+ )
+ output = JSON.stringify(val, null, 0);
+ else
+ output = String(val);
+
+ const lines = output.split('\n');
+ if (lines.length > 1)
+ return `${lines[0].trim()}${PLACEHOLDER}`;
+
+ return lines[0];
+ }
+
+ const lines = [
+ borderLine,
+ // The header
+ formatRow(
+ formatColumn('', indexColumnWidth),
+ columns.map(col =>
+ formatColumn(
+ col === Values ? 'Values' : String(col),
+ spacing
+ )
+ )
+ ),
+ borderLine,
+ // The rows
+ ...rows.map(rowKey => {
+ const row = tabularData[rowKey];
+
+ let line = formatRow(formatColumn(rowKey, indexColumnWidth), [
+ ...columns.map(colKey => {
+ /** @type {string} */
+ let col = '';
+
+ if (row !== null && typeof row === 'object') {
+ if (colKey in row)
+ col = formatValue(row[colKey]);
+ } else if (colKey === Values) {
+ col = formatValue(row);
+ }
+
+ return formatColumn(col, spacing);
+ }),
+ ]);
+
+ return line;
+ }),
+ borderLine,
+ ];
+
+ // @ts-expect-error
+ print(lines.join('\n'));
}
// 1.1.8 trace(...data)
diff --git a/util/console.cpp b/util/console.cpp
index 48af951c..323ed108 100644
--- a/util/console.cpp
+++ b/util/console.cpp
@@ -1,15 +1,41 @@
#include <config.h>
#include <stdio.h>
+#include <stdexcept>
+#include <string>
+
+#ifdef HAVE_READLINE_READLINE_H
+# include <readline/readline.h>
+#endif
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#elif defined(_WIN32)
# include <io.h>
+# include <windows.h>
#endif
+#include <glib.h>
+
#include "util/console.h"
+/**
+ * ANSI escape code sequences to manipulate terminals.
+ *
+ * See
+ * https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
+ */
+namespace ANSICode {
+/**
+ * ANSI escape code sequence to clear the terminal screen.
+ *
+ * Combination of 0x1B (Escape) and the sequence nJ where n=2,
+ * n=2 clears the entire display instead of only after the cursor.
+ */
+constexpr const char ESCAPE[] = "\x1b[2J";
+
+} // namespace ANSICode
+
#ifdef HAVE_UNISTD_H
const int Gjs::Console::stdin_fd = STDIN_FILENO;
const int Gjs::Console::stdout_fd = STDOUT_FILENO;
@@ -33,3 +59,42 @@ bool Gjs::Console::is_tty(int fd) {
return false;
#endif
}
+
+void Gjs::Console::size(int* width, int* height) {
+ {
+ const char* lines = g_getenv("LINES");
+ const char* columns = g_getenv("COLUMNS");
+ try {
+ *width = std::stoi(columns);
+ *height = std::stoi(lines);
+ return;
+ } catch (const std::invalid_argument& ia) {
+ } catch (const std::out_of_range& oor) {
+ }
+ }
+#ifdef HAVE_READLINE_READLINE_H
+ {
+ int rl_height, rl_width;
+ rl_get_screen_size(&rl_height, &rl_width);
+
+ if (rl_height > 0 && rl_width > 0) {
+ *height = rl_height;
+ *width = rl_width;
+ return;
+ }
+ }
+#elif defined(_WIN32)
+ {
+ CONSOLE_SCREEN_BUFFER_INFO csbi;
+ GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
+
+ *width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
+ *height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
+
+ return;
+ }
+#else
+ *height = -1;
+ *width = -1;
+#endif
+}
diff --git a/util/console.h b/util/console.h
index 442006d0..9facf313 100644
--- a/util/console.h
+++ b/util/console.h
@@ -11,6 +11,7 @@ extern const int stdin_fd;
extern const int stderr_fd;
[[nodiscard]] bool is_tty(int fd = stdout_fd);
+void size(int* width, int* height);
}; // namespace Console
}; // namespace Gjs
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]