[gjs/ewlsh/whatwg-console: 1/3] modules: Implement console.table()




commit 7f8a7ea7efd9686589dec28d6586fb20f472628d
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 | 210 ++++++++++++++++++++++++++++++++++++++++++++++++-
 util/console.cpp       |  70 ++++++++++++++++-
 util/console.h         |   1 +
 4 files changed, 308 insertions(+), 6 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 229684dd..a9ac6b9f 100644
--- a/modules/esm/console.js
+++ b/modules/esm/console.js
@@ -2,13 +2,43 @@
 // 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
+ */
+
+/**
+ * @returns {TerminalSize}
+ */
+export function getTerminalSize() {
+    try {
+        let 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 +170,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 2323189a..02509561 100644
--- a/util/console.cpp
+++ b/util/console.cpp
@@ -4,17 +4,42 @@
 #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"
 
 namespace Gjs {
 namespace Console {
+/**
+ * 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 stdin_fd = STDIN_FILENO;
@@ -40,5 +65,48 @@ bool is_tty(int fd) {
 #endif
 }
 
+void size(int* width, int* height) {
+    {
+        const char* lines = g_getenv("LINES");
+        const char* columns = g_getenv("COLUMNS");
+
+        // Check that lines and columns are not NULL
+        if (lines && columns) {
+            try {
+                *width = std::stoi(columns);
+                *height = std::stoi(lines);
+                return;
+            } catch (const std::invalid_argument& e) {
+            } catch (const std::out_of_range& e) {
+            }
+        }
+    }
+#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
+}
+
 }  // namespace Console
-}  // namespace Gjs
\ No newline at end of file
+}  // namespace Gjs
diff --git a/util/console.h b/util/console.h
index 275c6c7c..d3265e31 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]