[gjs/ewlsh/whatwg-console: 2/2] modules: Implement console.table()
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/whatwg-console: 2/2] modules: Implement console.table()
- Date: Sun, 13 Jun 2021 20:05:04 +0000 (UTC)
commit 0316774b26c285012431b8f3fd8a89ec4408b3ab
Author: Evan Welsh <contact evanwelsh com>
Date: Thu Jun 10 21:14:57 2021 -0700
modules: Implement console.table()
modules/console.cpp | 54 +++++++++++-
modules/esm/console.js | 233 ++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 283 insertions(+), 4 deletions(-)
---
diff --git a/modules/console.cpp b/modules/console.cpp
index 2ef511aa..b6d617fb 100644
--- a/modules/console.cpp
+++ b/modules/console.cpp
@@ -27,6 +27,16 @@
#include <glib.h>
#include <glib/gprintf.h> // for g_fprintf
+#if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_UNISTD_H)
+# include <fcntl.h>
+# include <stdio.h>
+# include <sys/ioctl.h>
+# include <unistd.h>
+# if defined(TIOCGWINSZ)
+# define GET_SIZE_USE_IOCTL
+# endif
+#endif
+
#include <js/CallArgs.h>
#include <js/CompilationAndEvaluation.h>
#include <js/CompileOptions.h>
@@ -276,13 +286,55 @@ gjs_console_interact(JSContext *context,
return true;
}
+bool gjs_console_get_terminal_size(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+ if (!obj)
+ return false;
+
+ // Use 'int' because Windows uses int values, whereas most Unix systems
+ // use 'short'
+ unsigned int width, height;
+
+ JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+#ifdef GET_SIZE_USE_IOCTL
+ struct winsize ws;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
+ gjs_throw(cx, "No terminal output is present.\n");
+ return false;
+ }
+
+ width = ws.ws_col;
+ height = ws.ws_row;
+#else
+ // TODO(ewlsh): Implement Windows equivalent.
+ // See
+ // https://docs.microsoft.com/en-us/windows/console/window-and-screen-buffer-size.
+ gjs_throw(cx, "Unable to retrieve terminal size on this platform.\n");
+ return false;
+#endif
+
+ 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;
+
+ argv.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 341fa31f..ece70a37 100644
--- a/modules/esm/console.js
+++ b/modules/esm/console.js
@@ -1,11 +1,66 @@
// eslint-disable-next-line
-/// <reference lib='es2019' />
+/// <reference lib='es2020' />
// @ts-check
// @ts-expect-error
import GLib from 'gi://GLib';
+export const {getTerminalSize: getNativeTerminalSize} =
+ // @ts-expect-error
+ import.meta.importSync('console');
+
+/**
+ * @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');
@@ -159,8 +214,180 @@ 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 it 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)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]