UNPKG

@kui-shell/core

Version:

An Electron-based shell for cloud-native development

377 lines • 14.5 kB
/* * 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