@kui-shell/core
Version:
An Electron-based shell for cloud-native development
377 lines • 14.5 kB
JavaScript
/*
* Copyright 2017 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Debug from 'debug';
const debug = Debug('main/headless-pretty-print');
debug('loading');
import colors from 'colors/safe';
import ElementMimic from '../util/element-mimic';
import { isTable } from '../webapp/models/table';
import { isMixedResponse, isMessageBearingEntity, isAbortableResponse } from '../models/entity';
import { isMultiModalResponse } from '../models/mmr/is';
import { isNavResponse } from '../models/NavResponse';
import { isHTML, isPromise } from '../util/types';
const log = console.log;
const error = console.error;
const colorAlways = process.argv.find(_ => _ === '--color=always');
const neverColor = process.argv.find(_ => _ === '--no-color' || _ === '--no-colors');
const rawOutput = process.argv.find(_ => _ === '--raw-output'); // don't try to pretty-print the JSON; c.f. jq's --raw-output
/**
* Determine whether how pretty we should make the output; if stdout
* is a pipe, then we shouldn't pass through colors, unless
* the user specified --color=always.
*
*/
const stdoutIsFIFO = process.platform !== 'win32' && process.stdout.isTTY;
const noColor = neverColor || (stdoutIsFIFO && !colorAlways);
debug('stdoutIsFIFO', stdoutIsFIFO, noColor);
const colorMap = {
'var(--color-brand-01)': colors.blue,
'var(--color-brand-02)': colors.blue,
'var(--color-support-02)': colors.blue,
'var(--color-black)': colors.black,
'var(--color-red)': colors.red,
'var(--color-green)': colors.green,
'var(--color-yellow)': colors.yellow,
'var(--color-blue)': colors.blue,
'var(--color-magenta)': colors.magenta,
'var(--color-cyan)': colors.cyan,
'var(--color-white)': colors.white,
'var(--color-gray)': colors.gray,
'var(--color-light-red)': colors.red,
'var(--color-light-green)': colors.green,
'var(--color-light-yellow)': colors.yellow
};
let graphicalShellIsOpen = false;
let graphicalShellIsPopup = false;
/** @return whether the state has changed as a result of this call */
export const setGraphicalShellIsOpen = (isPopup) => {
graphicalShellIsPopup = isPopup;
if (!graphicalShellIsOpen) {
graphicalShellIsOpen = true;
return true;
}
else {
return false;
}
};
class DefaultPrettyOptions {
}
let firstPrettyDom = true; // so we can avoid initial newlines for headers
const prettyDom = (dom, logger = log, stream = process.stdout, _color, { columnWidths, extraColor } = new DefaultPrettyOptions()) => {
debug('prettyDom');
const isHeader = dom.nodeType === 'h1' || dom.nodeType === 'h2' || dom.nodeType === 'h3' || dom.nodeType === 'h4';
const capitalize = false;
const hasMargin = dom.className.indexOf('bx--breadcrumb-item--slash') >= 0 ||
dom.className.indexOf('left-pad') >= 0 ||
dom.style['margin'] ||
dom.style['padding'];
if (hasMargin) {
stream.write(' ');
}
const extraColorChoice = isHeader || dom.hasStyle('fontWeight', 'bold')
? colors.bold
: dom.hasStyle('fontWeight', 500)
? colors.green
: dom.hasStyle('fontSize', '0.875em')
? colors.gray
: extraColor || colors.reset;
const colorFn = (dom.hasStyle('color') && colorMap[dom.style.color]) || _color;
// debug('colors', isHeader, colorCode, color, extraColorChoice)
if (isHeader) {
// an extra newline before headers
if (firstPrettyDom) {
// don't emit a header margin for the very first thing
// we print
firstPrettyDom = false;
}
else {
logger();
}
}
if (dom.innerText) {
const text = capitalize ? dom.innerText.charAt(0).toUpperCase() + dom.innerText.slice(1) : dom.innerText;
stream.write(extraColorChoice(colorFn(text)));
}
const newline = () => {
if (dom.nodeType === 'div' || isHeader) {
// not perfect, but treat divs as line breakers
logger();
}
};
if (hasMargin) {
stream.write(' ');
}
if (dom.innerText) {
newline();
}
// recurse to the children of this fake DOM
dom.children.forEach(child => prettyDom(child, logger, stream, _color, { extraColor: extraColorChoice }));
// handle table rows and cells:
if (dom.rows) {
// scan the table for max column widths
const columnWidths = [];
dom.rows.forEach(row => {
if (row.cells) {
row.cells.forEach((cell, idx) => {
const length = cell.recursiveInnerTextLength();
if (!columnWidths[idx])
columnWidths[idx] = length;
else
columnWidths[idx] = Math.max(columnWidths[idx], length);
});
}
});
dom.rows.forEach(row => {
prettyDom(row, logger, stream, _color, { columnWidths });
logger(); // insert a newline after every row
});
}
if (dom.cells) {
dom.cells.forEach((cell, idx) => {
prettyDom(cell, logger, stream, _color);
if (columnWidths && idx < dom.cells.length - 1) {
// pad out this column to the column width (except we don't
// need padding in the last column)
const slop = columnWidths[idx] - cell.recursiveInnerTextLength();
for (let jj = 0; jj < slop; jj++) {
stream.write(' ');
}
// and then a few more to separate the columns
stream.write(' ');
}
});
}
// trailing carriage return?
if (isHeader && !dom.innerText) {
logger();
}
};
/**
* Pretty print an object as JSON. This is mostly a stub for now, in
* case we want to do something fancier.
*
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const prettyJSON = (msg, logger = log) => {
const serialized = JSON.stringify(msg, undefined, 2);
logger(serialized);
};
/**
* Render a name with an optional package name
*
*/
const pn = (actionName, packageName) => colors.dim(`${packageName ? packageName + '/' : ''}`) + colors.blue(actionName);
/**
* Render a date; if it is from today, show just the time
*
*/
const prettyDate = (millis) => {
const date = new Date(millis);
const now = new Date();
if (date.getUTCFullYear() === now.getUTCFullYear() &&
date.getUTCMonth() === now.getUTCMonth() &&
date.getUTCDate() === now.getUTCDate()) {
return date.toLocaleTimeString();
}
else {
return date.toLocaleString();
}
};
/**
* Turn an entity into a row, because this entity came as part of an
* array of entities
*
*/
const pp = (_) => colors.dim(_ ? 'public' : 'private'); // pretty publish
const pk = (_) => colors.green(_.find(({ key }) => key === 'exec').value); // pretty kind
const rowifyAction = ({ name, packageName, publish, annotations, version }) => ({
name: pn(name, packageName),
'published?': pp(publish),
kind: pk(annotations),
version: colors.dim(version)
});
const rowifyPackage = ({ name, publish, binding }) => ({
name: pn(name),
'published?': pp(publish),
binding
});
const rowifySession = ({ sessionId, name, success, status, start }) => ({
sessionId,
app: pn(name),
start: colors.dim(prettyDate(start)),
status: status === 'pending' ? colors.yellow(status) : success ? colors.green(status) : colors.red(status)
});
const rowify = {
compositions: ({ name, packageName, prettyKind }) => ({
name: pn(name, packageName),
type: prettyKind
}),
session: rowifySession,
activations: ({ activationId, name }) => ({ activationId, name: pn(name) }),
actions: rowifyAction,
triggers: ({ name, publish }) => ({
name: pn(name),
'published?': pp(publish)
}),
packages: rowifyPackage,
plugins: ({ name, attributes }) => {
return {
name: pn(name),
version: colors.dim(attributes.find(({ key }) => key === 'version').value)
};
},
_default: ({ key, name, attributes }) => {
const row = {};
row[key || 'name'] = pn(name);
attributes.forEach(attr => {
if (!attr.placeholderValue) {
row[attr.key] = attr.value;
}
});
return row;
},
sequence: rowifyAction,
composer: rowifyAction,
binding: rowifyPackage,
live: rowifySession
};
/* function activationId(msg: Entity) {
return (msg as { activationId: string }).activationId
}
function name(msg: Entity) {
const entity = msg as { entity: { name: string } }
return entity.entity.name
} */
/**
* Pretty print routine that dispatches to the underlying smarter
* pretty printers (such as prettyDom and prettyjson)
*
*/
export const print = (msg, logger = log, stream = process.stdout, colorFn = colors.reset, ok = 'ok') => {
if (ok === 'error') {
colorFn = colors.red;
}
if (msg && (!graphicalShellIsOpen || graphicalShellIsPopup)) {
try {
if (msg === true) {
// true is the graphical shell's way of telling the repl to print 'ok'
debug('printing plain true');
logger(colors.green(ok));
}
else if (typeof msg === 'string' || typeof msg === 'boolean' || typeof msg === 'number') {
stream.write(msg);
}
else if (ElementMimic.isFakeDom(msg)) {
// msg is a DOM facade
debug('printing fake dom');
if (msg.className.indexOf('usage-error-wrapper') >= 0) {
// print usage errors to stdout
stream = process.stdout;
logger = log;
// color = 'reset'
}
prettyDom(msg, logger, stream, colorFn);
logger();
}
else if (isPromise(msg)) {
// msg is a promise; resolve it and try again
debug('printing promise');
return msg.then(msg => print(msg, logger, stream, colorFn, ok));
}
else if (isTable(msg)) {
if (msg.body.length === 0) {
return;
}
debug('printing table');
// strip off header row, as we'll make our own
const type = (msg.header && (msg.header.prettyType || msg.header.type)) ||
(msg.body.length > 0 && (msg.body[0].prettyType || msg.body[0].type));
const print = (type && rowify[type]) || rowify._default;
logger(require('columnify')(msg.body.map(print), {
headingTransform: (_) => colors.dim(_)
}));
}
else if (isMixedResponse(msg)) {
msg.forEach(_ => {
print(_);
stream.write('\n');
});
return logger(colors.green(ok));
}
else if (isMultiModalResponse(msg) || isNavResponse(msg)) {
throw new Error('cannot format this response in headless mode');
}
else if (Array.isArray(msg)) {
// msg is an array of stuff
debug('printing array');
if (msg.length > 0) {
try {
if (typeof msg[0] === 'string') {
// then we have a simple array of strings, not entities
;
msg.forEach(_ => {
const logline = _.split(/(stdout|stderr)/);
if (logline && logline.length === 3 && !rawOutput) {
// then this is a log line
const colorFn = logline[1] === 'stdout' ? colors.reset : colors.red;
logger(colors.dim(new Date(logline[0].trim()).toLocaleString()) + // timestamp
// + ' ' + colors.yellow(logline[1]) // stream (stdout, stderr)
colorFn(logline[2].replace(/^:/, ''))); // log message
}
else {
// otherwise, we're not sure what this is,
// so do not attempt to render it in a special way
logger(_);
}
});
return logger(colors.green(ok));
}
}
catch (err) {
error(err);
}
}
}
else if (isHTML(msg)) {
console.error('cannot format HTML message');
}
else if (isMessageBearingEntity(msg)) {
if (ElementMimic.isFakeDom(msg.message)) {
// msg.message is a DOM facade
prettyDom(msg.message, logger, stream, colorFn);
logger();
}
else {
logger(msg.message);
}
}
else if (isAbortableResponse(msg)) {
logger(msg.response);
}
else if (typeof msg === 'object') {
prettyJSON(msg, logger);
}
else {
logger(colorFn(msg));
}
}
catch (err) {
debug('got an error', err);
logger(colors.red(typeof msg === 'object' ? JSON.stringify(msg) : msg.toString()));
}
}
};
//# sourceMappingURL=headless-pretty-print.js.map