From ff721cbd823c30a295704157e355125cd0c7e5e1 Mon Sep 17 00:00:00 2001 From: scinorandex Date: Thu, 8 Apr 2021 09:07:17 +0800 Subject: [PATCH 1/5] Organize files --- src/api.ts | 6 ++--- src/api/plainRouter.ts | 4 ++-- src/api/{router.ts => regularRouter.ts} | 15 ++++++------ src/api/userAgent.ts | 2 +- src/cli.ts | 6 ++--- src/utils/{ => libs}/generateAsciichart.ts | 0 src/utils/{ => libs}/generateTable.ts | 0 src/utils/{ => libs}/getBoxArt.ts | 0 src/utils/{ => libs}/getResponses.ts | 2 +- src/utils/{ => libs}/getSaying.ts | 0 src/utils/{ => libs}/getTimestamp.ts | 0 .../{ => routes/plain}/generatePlainOutput.ts | 8 +++---- src/utils/{ => routes/plain}/plainHandlers.ts | 4 ++-- .../regular/generateRegularOutput.ts} | 10 ++++---- .../regular/regularHandlers.ts} | 23 +++++++++++++------ 15 files changed, 44 insertions(+), 36 deletions(-) rename src/api/{router.ts => regularRouter.ts} (86%) rename src/utils/{ => libs}/generateAsciichart.ts (100%) rename src/utils/{ => libs}/generateTable.ts (100%) rename src/utils/{ => libs}/getBoxArt.ts (100%) rename src/utils/{ => libs}/getResponses.ts (94%) rename src/utils/{ => libs}/getSaying.ts (100%) rename src/utils/{ => libs}/getTimestamp.ts (100%) rename src/utils/{ => routes/plain}/generatePlainOutput.ts (93%) rename src/utils/{ => routes/plain}/plainHandlers.ts (96%) rename src/utils/{generateOutput.ts => routes/regular/generateRegularOutput.ts} (85%) rename src/utils/{handlers.ts => routes/regular/regularHandlers.ts} (87%) diff --git a/src/api.ts b/src/api.ts index 978a078..50cb242 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,9 +2,9 @@ import express from "express"; import morgan from "morgan"; import { errorHandler } from "./api/errorHandler"; import { plainRouter } from "./api/plainRouter"; -import { router } from "./api/router"; +import { regularRouter } from "./api/regularRouter"; import { userAgentMiddleware } from "./api/userAgent"; -import { lines } from "./utils/getResponses"; +import { lines } from "./utils/libs/getResponses"; const port = parseInt(process.env.PORT!) || 7070; @@ -19,7 +19,7 @@ app.use(userAgentMiddleware); app.use(["/quiet/basic", "/quiet/cmd", "/quiet/plain"], plainRouter); app.use(["/basic", "/cmd", "/plain"], plainRouter); -app.use(["/quiet", "/"], router); +app.use(["/quiet", "/"], regularRouter); app.use("/", errorHandler); // Not found handler diff --git a/src/api/plainRouter.ts b/src/api/plainRouter.ts index c65d840..ddb2167 100644 --- a/src/api/plainRouter.ts +++ b/src/api/plainRouter.ts @@ -5,8 +5,8 @@ import { informationPerCountryPlain, historyPerCountryPlain, globalHistoryPlain, -} from "../utils/plainHandlers"; -import { isQuiet } from "./router"; +} from "../utils/routes/plain/plainHandlers"; +import { isQuiet } from "./regularRouter"; /** * The plainRouter handles all the plain routes such as /basic, /cmd, and /plain diff --git a/src/api/router.ts b/src/api/regularRouter.ts similarity index 86% rename from src/api/router.ts rename to src/api/regularRouter.ts index c767f3e..5fdb73d 100644 --- a/src/api/router.ts +++ b/src/api/regularRouter.ts @@ -4,7 +4,7 @@ import { globalInformation, historyPerCountry, informationPerCountry, -} from "../utils/handlers"; +} from "../utils/routes/regular/regularHandlers"; import handleAsync from "./handleAsync"; /** @@ -16,13 +16,12 @@ export const isQuiet: (req: Request) => boolean = (req) => req.baseUrl.startsWith("/quiet"); /** - * The rootRouter handles all the processing of the requests *after* passing through + * The regularRouter handles all the processing of the requests *after* passing through * all middlewares except not found and error handling middleware */ -export const router = Router({ mergeParams: true }); +export const regularRouter = Router({ mergeParams: true }); -// rootRouter.get("/history/:country/:type", historyPerCountryAndType); -router.get( +regularRouter.get( "/history/:mode?", handleAsync(async (req, res, next) => { // get mode from params @@ -37,7 +36,7 @@ router.get( }) ); -router.get( +regularRouter.get( "/history/:country/:mode?", handleAsync(async (req, res, next) => { const country = req.params.country; @@ -53,7 +52,7 @@ router.get( }) ); -router.get( +regularRouter.get( "/:country", handleAsync(async (req, res, _next) => { const country = req.params.country; @@ -61,7 +60,7 @@ router.get( }) ); -router.get( +regularRouter.get( "/", handleAsync(async (req, res, _next) => { res.send(await globalInformation(isQuiet(req))); diff --git a/src/api/userAgent.ts b/src/api/userAgent.ts index c1f39a1..6347feb 100644 --- a/src/api/userAgent.ts +++ b/src/api/userAgent.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from "express"; -import { lines } from "../utils/getResponses"; +import { lines } from "../utils/libs/getResponses"; // Type of middleware and handler export type Handler = ( diff --git a/src/cli.ts b/src/cli.ts index f00fbf6..6d56d4a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,18 +1,18 @@ #!/usr/bin/env node import argv from "minimist"; -import { lines, welcomeMessage } from "./utils/getResponses"; +import { lines, welcomeMessage } from "./utils/libs/getResponses"; import { globalHistory, globalInformation, historyPerCountry, informationPerCountry, -} from "./utils/handlers"; +} from "./utils/routes/regular/regularHandlers"; import { globalHistoryPlain, globalInformationPlain, historyPerCountryPlain, informationPerCountryPlain, -} from "./utils/plainHandlers"; +} from "./utils/routes/plain/plainHandlers"; const args = argv(process.argv.slice(2)); let { history, mode, help, quiet, plain } = args; diff --git a/src/utils/generateAsciichart.ts b/src/utils/libs/generateAsciichart.ts similarity index 100% rename from src/utils/generateAsciichart.ts rename to src/utils/libs/generateAsciichart.ts diff --git a/src/utils/generateTable.ts b/src/utils/libs/generateTable.ts similarity index 100% rename from src/utils/generateTable.ts rename to src/utils/libs/generateTable.ts diff --git a/src/utils/getBoxArt.ts b/src/utils/libs/getBoxArt.ts similarity index 100% rename from src/utils/getBoxArt.ts rename to src/utils/libs/getBoxArt.ts diff --git a/src/utils/getResponses.ts b/src/utils/libs/getResponses.ts similarity index 94% rename from src/utils/getResponses.ts rename to src/utils/libs/getResponses.ts index 95ed68d..0b0e682 100644 --- a/src/utils/getResponses.ts +++ b/src/utils/libs/getResponses.ts @@ -1,4 +1,4 @@ -const { version } = require("../../package.json"); +const { version } = require("../../../package.json"); // This file is a centralized location to get responses to the user // Such as the BMC link, the twitter link, github repo page etc. diff --git a/src/utils/getSaying.ts b/src/utils/libs/getSaying.ts similarity index 100% rename from src/utils/getSaying.ts rename to src/utils/libs/getSaying.ts diff --git a/src/utils/getTimestamp.ts b/src/utils/libs/getTimestamp.ts similarity index 100% rename from src/utils/getTimestamp.ts rename to src/utils/libs/getTimestamp.ts diff --git a/src/utils/generatePlainOutput.ts b/src/utils/routes/plain/generatePlainOutput.ts similarity index 93% rename from src/utils/generatePlainOutput.ts rename to src/utils/routes/plain/generatePlainOutput.ts index 6ee16b6..36d8312 100644 --- a/src/utils/generatePlainOutput.ts +++ b/src/utils/routes/plain/generatePlainOutput.ts @@ -1,7 +1,7 @@ -import { PlainData } from "./getInformation"; -import { lines } from "./getResponses"; -import { getSaying } from "./getSaying"; -import { getTimestamp } from "./getTimestamp"; +import { PlainData } from "../../getInformation"; +import { lines } from "../../libs/getResponses"; +import { getSaying } from "../../libs/getSaying"; +import { getTimestamp } from "../../libs/getTimestamp"; /** * @param info The plain data that will be shown at the top in two columns diff --git a/src/utils/plainHandlers.ts b/src/utils/routes/plain/plainHandlers.ts similarity index 96% rename from src/utils/plainHandlers.ts rename to src/utils/routes/plain/plainHandlers.ts index 27a7c69..d4430fe 100644 --- a/src/utils/plainHandlers.ts +++ b/src/utils/routes/plain/plainHandlers.ts @@ -1,11 +1,11 @@ -import { generateAsciichart } from "./generateAsciichart"; +import { generateAsciichart } from "../../libs/generateAsciichart"; import { generatePlainOutput } from "./generatePlainOutput"; import { getAllInfo, getCountryInfo, getHistorical, PlainData, -} from "./getInformation"; +} from "../../getInformation"; /** * globalHistory shows a tablechart of the cases of all the countries diff --git a/src/utils/generateOutput.ts b/src/utils/routes/regular/generateRegularOutput.ts similarity index 85% rename from src/utils/generateOutput.ts rename to src/utils/routes/regular/generateRegularOutput.ts index 238ea02..800f491 100644 --- a/src/utils/generateOutput.ts +++ b/src/utils/routes/regular/generateRegularOutput.ts @@ -1,7 +1,7 @@ -import { generateColorTable } from "./generateTable"; -import { getTimestamp } from "./getTimestamp"; -import { getSaying } from "./getSaying"; -import { lines } from "./getResponses"; +import { generateColorTable } from "../../libs/generateTable"; +import { getTimestamp } from "../../libs/getTimestamp"; +import { getSaying } from "../../libs/getSaying"; +import { lines } from "../../libs/getResponses"; /** * @@ -11,7 +11,7 @@ import { lines } from "./getResponses"; * @param quiet Optional, set to true if the user does not want unnecessary information * @returns A string containing a formatted table */ -export const generateOutput: ( +export const generateRegularOutput: ( chartType: string, updateTime: number, data: (string | string[])[], diff --git a/src/utils/handlers.ts b/src/utils/routes/regular/regularHandlers.ts similarity index 87% rename from src/utils/handlers.ts rename to src/utils/routes/regular/regularHandlers.ts index e6067b1..0c8a110 100644 --- a/src/utils/handlers.ts +++ b/src/utils/routes/regular/regularHandlers.ts @@ -1,6 +1,10 @@ -import { generateOutput } from "./generateOutput"; -import { generateAsciichart } from "./generateAsciichart"; -import { getAllInfo, getCountryInfo, getHistorical } from "./getInformation"; +import { generateRegularOutput } from "./generateRegularOutput"; +import { generateAsciichart } from "../../libs/generateAsciichart"; +import { + getAllInfo, + getCountryInfo, + getHistorical, +} from "../../getInformation"; /** * historyPerCountry shows a tablechart of the of a country @@ -29,7 +33,7 @@ export const historyPerCountry: ( rows = rows.concat(chart); // Generate table - let response = generateOutput( + let response = generateRegularOutput( `${countryName} Historical Chart`, updated, rows, @@ -63,7 +67,7 @@ export const globalHistory: ( rows.push(historicalData.date.magenta); rows = rows.concat(chart); - let response = generateOutput( + let response = generateRegularOutput( "Global Historical Chart", updated, rows, @@ -88,7 +92,7 @@ export const informationPerCountry: ( let [updated, _, countryName, rows] = (await getCountryInfo(country)) as [ number, string, string, (string[] | string)[]]; - let response = generateOutput( + let response = generateRegularOutput( `${countryName} Update`, updated, rows, @@ -112,7 +116,12 @@ export const globalInformation: (quiet: boolean) => Promise = async ( (string[] | string)[] ]; - let response = generateOutput("Global Update", updated, rowsOfData, quiet); + let response = generateRegularOutput( + "Global Update", + updated, + rowsOfData, + quiet + ); return response; }; From 61d482f3a07844c5d911046c5fe04fc06cba0dec Mon Sep 17 00:00:00 2001 From: scinorandex Date: Sat, 10 Apr 2021 02:43:47 +0800 Subject: [PATCH 2/5] refactor existing code --- src/utils/getInformation.ts | 236 ++++++++---------- src/utils/libs/capitalizeFirstLetter.ts | 8 + src/utils/libs/convertCountry.ts | 22 ++ src/utils/libs/numberNormalizers.ts | 21 ++ src/utils/routes/plain/generatePlainOutput.ts | 12 +- src/utils/routes/plain/plainHandlers.ts | 46 ++-- src/utils/routes/plain/plainParser.ts | 103 ++++++++ src/utils/routes/regular/regularHandlers.ts | 61 ++--- src/utils/routes/regular/regularParser.ts | 79 ++++++ 9 files changed, 392 insertions(+), 196 deletions(-) create mode 100644 src/utils/libs/capitalizeFirstLetter.ts create mode 100644 src/utils/libs/convertCountry.ts create mode 100644 src/utils/libs/numberNormalizers.ts create mode 100644 src/utils/routes/plain/plainParser.ts create mode 100644 src/utils/routes/regular/regularParser.ts diff --git a/src/utils/getInformation.ts b/src/utils/getInformation.ts index 9757690..90d9249 100644 --- a/src/utils/getInformation.ts +++ b/src/utils/getInformation.ts @@ -1,61 +1,38 @@ import axios from "axios"; +import { convertCountryCode } from "../utils/libs/convertCountry"; +import { capitalizeFirstLetter } from "../utils/libs/capitalizeFirstLetter"; axios.defaults.baseURL = "https://disease.sh/v3/covid-19"; -let countryCodes: { [key: string]: string } = {}; -(async () => { - countryCodes = (await axios.get("http://country.io/names.json")).data; -})(); - -export interface PlainData { - data: { - [key: string]: string; - }; - metainfo: { - [key: string]: number | string; - }; -} - /** * @param isPlain Set to true to recieve an object containing the responses instead of the rows * @returns an object containing the data and metainfo **if isPlain is set to true** * @returns an array in the format of [timestamp, rows] **if isPlain is set to false** */ -export const getAllInfo: ( - isPlain?: boolean -) => Promise<[number, (string[] | string)[]] | PlainData> = async ( - isPlain = false -) => { +export const getAllInfo: () => Promise<{ + updated: number; + data: { + cases: number; + deaths: number; + recovered: number; + deathRate: number; + recoveryRate: number; + }; +}> = async () => { let { data: globalData } = await axios.get("/all"); let { cases, deaths, recovered, updated } = globalData; + let deathRate = (deaths / cases) * 100; + let recoveryRate = (recovered / cases) * 100; - let mortalityPercentage = ((deaths / cases) * 100).toFixed(2) + "%"; - let recoveredPercentage = ((recovered / cases) * 100).toFixed(2) + "%"; - - [cases, deaths, recovered] = [cases, deaths, recovered].map((num: number) => - num.toLocaleString("en-US", { maximumFractionDigits: 0 }) - ); - - // Return object containing information if isPlain is set to true - if (isPlain) { - return { - data: { - Cases: cases, - Deaths: deaths, - "Mortality %": mortalityPercentage, - Recovered: recovered, - "Recovered %": recoveredPercentage, - }, - metainfo: { - updated, - }, - }; - } - - // Return rows if isPlain is set to false - // prettier-ignore - return [updated, [ - ["Cases".magenta, "Deaths".red,"Recovered".green, "Mortality %".red,"Recovered %".green], - [cases, deaths, recovered, mortalityPercentage, recoveredPercentage]]] + return { + updated, + data: { + cases, + deaths, + recovered, + deathRate, + recoveryRate, + }, + }; }; /** @@ -65,107 +42,108 @@ export const getAllInfo: ( * @returns an array in the format of [timestamp, API countryname, formal countryname, rows[]] **if isPlain is false** */ export const getCountryInfo: ( - country: string, - isPlain?: boolean -) => Promise< - [number, string, string, (string[] | string)[]] | PlainData -> = async (country, isPlain) => { - // Wait 1 second for countryCodes to initialize, needed for CLI - if (Object.keys(countryCodes).length === 0) { - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - } - - country = - country.length < 3 ? countryCodes[country.toUpperCase()] : country; // Convert country code to country name - - if (country === undefined || typeof country === "undefined") - throw new Error(`Cannot find provided country`); + country: string +) => Promise<{ + updated: number; + formalCountryName: string; + apiCountryName: string; + data: { + cases: number; + todayCases: number; + active: number; + recovered: number; + deaths: number; + todayDeaths: number; + critical: number; + deathRate: number; + recoveryRate: number; + casesPerOneMillion: number; + }; +}> = async (country) => { + // Convert country to country code + country = await convertCountryCode(country); + let formalCountryName = capitalizeFirstLetter(country); try { let { data: countryData } = await axios.get(`/countries/${country}`); // prettier-ignore - let { country: countryName, updated, cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical} = countryData; - - let mortalityPercentage = ((deaths / cases) * 100).toFixed(2) + "%"; - let recoveredPercentage = ((recovered / cases) * 100).toFixed(2) + "%"; - - // prettier-ignore - [ cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical ] = - [ cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical, - ].map((num: number) => - num.toLocaleString("en-US", { maximumFractionDigits: 0 }) - ); - - // Return object containing information if isPlain is set to true - if (isPlain) { - return { - data: { - Cases: cases, - "Today Cases": todayCases, - Active: active, - Recovered: recovered, - Deaths: deaths, - "Today Deaths": todayDeaths, - Critical: critical, - "Mortality %": mortalityPercentage, - "Recovery %": recoveredPercentage, - "Cases/Million": casesPerOneMillion, - }, - metainfo: { - updated, - countryName, - }, - }; - } + let { country: apiCountryName, updated, cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical} = countryData; + let deathRate = (deaths / cases) * 100; + let recoveryRate = (recovered / cases) * 100; - //prettier-ignore - return [updated, country, countryName, [ - [ "Cases".magenta, "Deaths".red, "Recovered".green, "Active".blue, "Cases/Million".blue,], - [ cases, deaths, recovered, active, casesPerOneMillion,], - [ "Today Cases".magenta, "Today Deaths".red, "Critical".red, "Mortaility %".red, "Recovery %".green], - [ todayCases, todayDeaths, critical, mortalityPercentage, recoveredPercentage]] - ] + return { + updated, + formalCountryName, + apiCountryName, + // prettier-ignore + data: { + cases, todayCases, active, recovered, deaths, todayDeaths, critical, deathRate, recoveryRate, casesPerOneMillion + }, + }; } catch { throw new Error(`Cannot find the provided country`); } }; -/** - * Get historical info about a country / the world - * @param mode - mode that the user requested - * @param country - countryname that the user requested, leave blank to get world data - * @returns an object containing date and chartData properties - */ -export const getHistorical: ( - mode: "cases" | "deaths" | "recovered", +type getHistoricalMode = "cases" | "deaths" | "recovered" | "all"; + +// prettier-ignore +export async function getHistorical( + mode: T, country?: string -) => Promise<{ +): Promise< + T extends "all" ? { + date: string; + chartData: { + [key: string]: { + [key: string]: number; + }; + }; + } : { + date: string; + chartData: number[]; + } +>; + +export async function getHistorical( + mode: getHistoricalMode, + country = "all" +): Promise<{ date: string; - chart: number[]; -}> = async (mode, country = "all") => { + chartData: + | number[] + | { + [key: string]: { + [key: string]: number; + }; + }; +}> { const { data: historicalData } = await axios.get(`/historical/${country}`); - const data: { - [key: string]: number; - } = - country === "all" - ? historicalData[mode] - : historicalData["timeline"][mode]; + // Get all the modes + let chartData = + country === "all" ? historicalData : historicalData["timeline"]; + + // If the user did not select all, then get the mode they wanted + if (mode !== "all") chartData = chartData[mode]; // Get first and last date - const dates = Object.keys(data); + const dates = Object.keys(mode === "all" ? chartData["cases"] : chartData); // Label for chart const date = `${ mode.charAt(0).toUpperCase() + mode.slice(1) } from ${dates.shift()} to ${dates.pop()}`; - const chartData = Object.values(data); - - return { - date, - chart: chartData, - }; -}; + if (mode === "all") { + return { + date, + chartData, + }; + } else { + return { + date, + chartData: Object.values(chartData) as number[], + }; + } +} diff --git a/src/utils/libs/capitalizeFirstLetter.ts b/src/utils/libs/capitalizeFirstLetter.ts new file mode 100644 index 0000000..dfb956c --- /dev/null +++ b/src/utils/libs/capitalizeFirstLetter.ts @@ -0,0 +1,8 @@ +/** + * + * @param str String that you want to capitalize + * @returns String with first letter capitalized + */ +export const capitalizeFirstLetter: (str: string) => string = (str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; diff --git a/src/utils/libs/convertCountry.ts b/src/utils/libs/convertCountry.ts new file mode 100644 index 0000000..2551097 --- /dev/null +++ b/src/utils/libs/convertCountry.ts @@ -0,0 +1,22 @@ +import axios from "axios"; +let countryCodes: { [key: string]: string } = {}; +(async () => { + countryCodes = (await axios.get("http://country.io/names.json")).data; +})(); + +export const convertCountryCode: (country: string) => Promise = async ( + country +) => { + // Wait 1 second for countryCodes to initialize, needed for CLI + if (Object.keys(countryCodes).length === 0) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + } + + if (country.length < 3) country = countryCodes[country.toUpperCase()]; + + if (country === undefined || typeof country === "undefined") + throw new Error(`Cannot find provided country`); + else return country; +}; diff --git a/src/utils/libs/numberNormalizers.ts b/src/utils/libs/numberNormalizers.ts new file mode 100644 index 0000000..563ee91 --- /dev/null +++ b/src/utils/libs/numberNormalizers.ts @@ -0,0 +1,21 @@ +/** + * Takes a number, fixes it to 2 decimal places, and converts it into a string with a % + * @param number Number that you want to convert to percentage + * @returns String containing percentaged number + */ +export const convertToPercentage: (...args: number[]) => string[] = ( + ...args +) => { + return args.map((number) => number.toFixed(2) + "%"); +}; + +/** + * Takes numbers and adds commas every 3 digits starting from the right + * @param args Numbers that you want to normalize + * @returns Array of strings containing the normalized numbers + */ +export const normalizeNumbers: (...args: number[]) => string[] = (...args) => { + return args.map((number) => + number.toLocaleString("en-US", { maximumFractionDigits: 0 }) + ); +}; diff --git a/src/utils/routes/plain/generatePlainOutput.ts b/src/utils/routes/plain/generatePlainOutput.ts index 36d8312..9d44588 100644 --- a/src/utils/routes/plain/generatePlainOutput.ts +++ b/src/utils/routes/plain/generatePlainOutput.ts @@ -1,4 +1,3 @@ -import { PlainData } from "../../getInformation"; import { lines } from "../../libs/getResponses"; import { getSaying } from "../../libs/getSaying"; import { getTimestamp } from "../../libs/getTimestamp"; @@ -11,17 +10,22 @@ import { getTimestamp } from "../../libs/getTimestamp"; * @returns A string showing the provided data and configuration */ export const generatePlainOutput: ( - info: PlainData, + info: { + data: { + [key: string]: string; + }; + timeUpdated: number; + }, chartType: string, quiet: boolean, extraRows?: string[] -) => string = ({ data, metainfo }, chartType, quiet, extraRows) => { +) => string = ({ data, timeUpdated }, chartType, quiet, extraRows) => { // Set line depending if it contains a chart or not let line = extraRows === undefined ? "-".repeat(60) : "-".repeat(68); line += "\n"; let header = `${lines.defaultHeader} - ${chartType}`; - let timestamp = getTimestamp(metainfo.updated as number); + let timestamp = getTimestamp(timeUpdated); let saying = getSaying(); // Generate table diff --git a/src/utils/routes/plain/plainHandlers.ts b/src/utils/routes/plain/plainHandlers.ts index d4430fe..fe8af3d 100644 --- a/src/utils/routes/plain/plainHandlers.ts +++ b/src/utils/routes/plain/plainHandlers.ts @@ -1,11 +1,7 @@ import { generateAsciichart } from "../../libs/generateAsciichart"; import { generatePlainOutput } from "./generatePlainOutput"; -import { - getAllInfo, - getCountryInfo, - getHistorical, - PlainData, -} from "../../getInformation"; +import { countryInfoPlain, globalInfoPlain } from "./plainParser"; +import { getHistorical } from "../../getInformation"; /** * globalHistory shows a tablechart of the cases of all the countries @@ -19,16 +15,16 @@ export const globalHistoryPlain: ( quiet: boolean ) => Promise = async (mode, quiet) => { // Get summary info - const info = (await getAllInfo(true)) as PlainData; + const info = await globalInfoPlain(); // Get data from API - const historicalData = await getHistorical(mode); + const { date, chartData } = await getHistorical(mode); // Generate historical graph - const chart = generateAsciichart(historicalData.chart, true, 7); + const chart = generateAsciichart(chartData, true, 7); return generatePlainOutput(info, `Global Historical Chart`, quiet, [ - historicalData.date, + date, chart, ]); }; @@ -48,22 +44,18 @@ export const historyPerCountryPlain: ( quiet: boolean ) => Promise = async (country, mode, quiet) => { // Get summary info about a country - const info = (await getCountryInfo(country, true)) as PlainData; + const info = await countryInfoPlain(country); + const { apiCountryName, formalCountryName } = info; - const historicalData = await getHistorical( - mode, - info.metainfo.countryName as string - ); + const { date, chartData } = await getHistorical(mode, apiCountryName); // Generate historical graph - const chart = generateAsciichart(historicalData.chart, true, 7); + const chart = generateAsciichart(chartData, true, 7); - return generatePlainOutput( - info, - `${info.metainfo.countryName} Chart`, - quiet, - [historicalData.date, chart] - ); + return generatePlainOutput(info, `${formalCountryName} Chart`, quiet, [ + date, + chart, + ]); }; /** @@ -77,12 +69,8 @@ export const informationPerCountryPlain: ( country: string, quiet: boolean ) => Promise = async (country, quiet) => { - const info = (await getCountryInfo(country, true)) as PlainData; - return generatePlainOutput( - info, - `${info.metainfo.countryName} Update`, - quiet - ); + const info = await countryInfoPlain(country); + return generatePlainOutput(info, `${info.formalCountryName} Update`, quiet); }; /** @@ -93,6 +81,6 @@ export const informationPerCountryPlain: ( export const globalInformationPlain: ( quiet: boolean ) => Promise = async (quiet) => { - const info = (await getAllInfo(true)) as PlainData; + const info = await globalInfoPlain(); return generatePlainOutput(info, "Global Update", quiet); }; diff --git a/src/utils/routes/plain/plainParser.ts b/src/utils/routes/plain/plainParser.ts new file mode 100644 index 0000000..516e93a --- /dev/null +++ b/src/utils/routes/plain/plainParser.ts @@ -0,0 +1,103 @@ +import { getAllInfo, getCountryInfo } from "../../getInformation"; +import { + convertToPercentage, + normalizeNumbers, +} from "../../libs/numberNormalizers"; + +export const globalInfoPlain: () => Promise<{ + timeUpdated: number; + data: { + Cases: string; + Deaths: string; + "Mortality %": string; + Recovered: string; + "Recovered %": string; + }; +}> = async () => { + const { updated, data } = await getAllInfo(); + // Destructure data + const { cases, deathRate, deaths, recovered, recoveryRate } = data; + + // Normalize and convert to percentages + const [stringCases, stringDeaths, stringRecovered] = normalizeNumbers( + cases, + deaths, + recovered + ); + const [stringDeathRate, stringRecoveryRate] = convertToPercentage( + deathRate, + recoveryRate + ); + + return { + timeUpdated: updated, + data: { + Cases: stringCases, + Deaths: stringDeaths, + "Mortality %": stringDeathRate, + Recovered: stringRecovered, + "Recovered %": stringRecoveryRate, + }, + }; +}; + +export const countryInfoPlain: ( + country: string +) => Promise<{ + timeUpdated: number; + apiCountryName: string; + formalCountryName: string; + data: { + Cases: string; + "Today Cases": string; + Active: string; + Recovered: string; + Deaths: string; + "Today Deaths": string; + Critical: string; + "Mortality %": string; + "Recovery %": string; + "Cases/Million": string; + }; +}> = async (country) => { + // prettier-ignore + const { updated, formalCountryName, data, apiCountryName } = await getCountryInfo(country); + // prettier-ignore + let {active, cases, casesPerOneMillion, critical, deathRate, deaths,recovered, recoveryRate, todayCases, todayDeaths} = data; + + let [deathRatePercent, recoveryRatePercent] = convertToPercentage( + deathRate, + recoveryRate + ); + + // Disgusting code + // prettier-ignore + let [ + stringCases, + stringDeaths, + stringRecovered, + stringActive, + stringCasesPerMillion, + stringTodayCases, + stringTodayDeaths, + stringCritical + ] = normalizeNumbers(cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical); + + return { + timeUpdated: updated, + apiCountryName, + formalCountryName, + data: { + Cases: stringCases, + "Today Cases": stringTodayCases, + Active: stringActive, + Recovered: stringRecovered, + Deaths: stringDeaths, + "Today Deaths": stringTodayDeaths, + Critical: stringCritical, + "Mortality %": deathRatePercent, + "Recovery %": recoveryRatePercent, + "Cases/Million": stringCasesPerMillion, + }, + }; +}; diff --git a/src/utils/routes/regular/regularHandlers.ts b/src/utils/routes/regular/regularHandlers.ts index 0c8a110..dbd95ae 100644 --- a/src/utils/routes/regular/regularHandlers.ts +++ b/src/utils/routes/regular/regularHandlers.ts @@ -1,10 +1,7 @@ import { generateRegularOutput } from "./generateRegularOutput"; import { generateAsciichart } from "../../libs/generateAsciichart"; -import { - getAllInfo, - getCountryInfo, - getHistorical, -} from "../../getInformation"; +import { getHistorical } from "../../getInformation"; +import { countryInfo, globalInfo } from "./regularParser"; /** * historyPerCountry shows a tablechart of the of a country @@ -20,23 +17,26 @@ export const historyPerCountry: ( quiet: boolean ) => Promise = async (country, mode, quiet) => { // Get summary info about a country - let [updated, apiCountryname, countryName, rows] = (await getCountryInfo( - country - )) as [number, string, string, (string[] | string)[]]; + let { + apiCountryName, + formalCountryName, + rowsOfData, + timeUpdated, + } = await countryInfo(country); // Fetch chart data and generate historical graph; - let historicalData = await getHistorical(mode, apiCountryname); - const chart = generateAsciichart(historicalData.chart).split("\n"); + let historicalData = await getHistorical(mode, apiCountryName); + const chart = generateAsciichart(historicalData.chartData).split("\n"); // add chart label and chart - rows.push(historicalData.date.magenta); - rows = rows.concat(chart); + rowsOfData.push(historicalData.date.magenta); + rowsOfData = rowsOfData.concat(chart); // Generate table let response = generateRegularOutput( - `${countryName} Historical Chart`, - updated, - rows, + `${formalCountryName} Historical Chart`, + timeUpdated, + rowsOfData, quiet ); @@ -55,22 +55,19 @@ export const globalHistory: ( quiet: boolean ) => Promise = async (mode, quiet) => { // Get summary info - let [updated, rows] = (await getAllInfo()) as [ - number, - (string[] | string)[] - ]; + let { timeUpdated, rowsOfData } = await globalInfo(); // Fetch chart data and generate historical graph; const historicalData = await getHistorical(mode); - const chart = generateAsciichart(historicalData.chart).split("\n"); + const chart = generateAsciichart(historicalData.chartData).split("\n"); - rows.push(historicalData.date.magenta); - rows = rows.concat(chart); + rowsOfData.push(historicalData.date.magenta); + rowsOfData = rowsOfData.concat(chart); let response = generateRegularOutput( "Global Historical Chart", - updated, - rows, + timeUpdated, + rowsOfData, quiet ); @@ -89,13 +86,12 @@ export const informationPerCountry: ( quiet: boolean ) => Promise = async (country, quiet) => { // prettier-ignore - let [updated, _, countryName, rows] = (await getCountryInfo(country)) as [ - number, string, string, (string[] | string)[]]; + let {timeUpdated, formalCountryName, rowsOfData} = await countryInfo(country); let response = generateRegularOutput( - `${countryName} Update`, - updated, - rows, + `${formalCountryName} Update`, + timeUpdated, + rowsOfData, quiet ); @@ -111,14 +107,11 @@ export const informationPerCountry: ( export const globalInformation: (quiet: boolean) => Promise = async ( quiet ) => { - const [updated, rowsOfData] = (await getAllInfo()) as [ - number, - (string[] | string)[] - ]; + const { timeUpdated, rowsOfData } = await globalInfo(); let response = generateRegularOutput( "Global Update", - updated, + timeUpdated, rowsOfData, quiet ); diff --git a/src/utils/routes/regular/regularParser.ts b/src/utils/routes/regular/regularParser.ts new file mode 100644 index 0000000..e13731b --- /dev/null +++ b/src/utils/routes/regular/regularParser.ts @@ -0,0 +1,79 @@ +import { getAllInfo, getCountryInfo } from "../../getInformation"; +import { + convertToPercentage, + normalizeNumbers, +} from "../../libs/numberNormalizers"; + +export const globalInfo: () => Promise<{ + timeUpdated: number; + rowsOfData: (string | string[])[]; +}> = async () => { + // Get raw data from getAllInfo + const { updated, data } = await getAllInfo(); + let { cases, deaths, recovered, deathRate, recoveryRate } = data; + + // Parse data and convert into rows + let dataInStringArray = normalizeNumbers(cases, deaths, recovered); + let [deathRatePercent, recoveryRatePercent] = convertToPercentage( + deathRate, + recoveryRate + ); + + dataInStringArray.push(deathRatePercent, recoveryRatePercent); + + let rowsOfData = [ + [ + "Cases".magenta, + "Deaths".red, + "Recovered".green, + "Mortality %".red, + "Recovered %".green, + ], + dataInStringArray, + ]; + + return { timeUpdated: updated, rowsOfData }; +}; + +export const countryInfo: ( + country: string +) => Promise<{ + timeUpdated: number; + formalCountryName: string; + apiCountryName: string; + rowsOfData: (string | string[])[]; +}> = async (country) => { + // prettier-ignore + const { updated, formalCountryName, data, apiCountryName } = await getCountryInfo(country); + // prettier-ignore + let {active, cases, casesPerOneMillion, critical, deathRate, deaths,recovered, recoveryRate, todayCases, todayDeaths} = data; + + let [deathRatePercent, recoveryRatePercent] = convertToPercentage( + deathRate, + recoveryRate + ); + + // prettier-ignore + let normalizedNumbers = normalizeNumbers(cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical); + // First row contains the first 5 arguments that were passed to normalizeNumbers + let firstRow = normalizedNumbers.slice(0, 5); + // Second row contains the rest of the arguments + let secondRow = normalizedNumbers.slice(5); + // Then we push the percentages + secondRow.push(deathRatePercent, recoveryRatePercent); + + // prettier-ignore + let rowsOfData: (string | string[])[] = [ + [ "Cases".magenta, "Deaths".red, "Recovered".green, "Active".blue, "Cases/Million".blue,], + firstRow, + [ "Today Cases".magenta, "Today Deaths".red, "Critical".red, "Mortaility %".red, "Recovery %".green], + secondRow + ]; + + return { + timeUpdated: updated, + formalCountryName, + apiCountryName, + rowsOfData, + }; +}; From a9eeb1b6cfa914e91a5ad7e842d6639981cb111d Mon Sep 17 00:00:00 2001 From: scinorandex Date: Mon, 12 Apr 2021 02:29:00 +0800 Subject: [PATCH 3/5] Initial support for dashboard --- package.json | 6 +- src/api.ts | 5 + src/api/dashboardRouter.ts | 54 +++ src/api/userAgent.ts | 4 +- src/utils/getInformation.ts | 11 +- src/utils/libs/columnizeData.ts | 40 ++ src/utils/routes/dashboard/blessedConfig.ts | 115 ++++++ .../routes/dashboard/dashboardHandlers.ts | 223 +++++++++++ .../dashboard/generateDashboardOutput.ts | 204 ++++++++++ .../routes/dashboard/generateWebDashboard.ts | 33 ++ src/utils/routes/plain/generatePlainOutput.ts | 23 +- yarn.lock | 364 +++++++++++++++++- 12 files changed, 1053 insertions(+), 29 deletions(-) create mode 100644 src/api/dashboardRouter.ts create mode 100644 src/utils/libs/columnizeData.ts create mode 100644 src/utils/routes/dashboard/blessedConfig.ts create mode 100644 src/utils/routes/dashboard/dashboardHandlers.ts create mode 100644 src/utils/routes/dashboard/generateDashboardOutput.ts create mode 100644 src/utils/routes/dashboard/generateWebDashboard.ts diff --git a/package.json b/package.json index e714dce..103b7f7 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "license": "GPL-3.0", "devDependencies": { "@types/asciichart": "^1.5.4", + "@types/blessed": "^0.1.17", "@types/express": "^4.17.11", "@types/minimist": "^1.2.1", "@types/morgan": "^1.9.2", @@ -43,10 +44,13 @@ "dependencies": { "asciichart": "^1.5.25", "axios": "^0.21.1", + "blessed": "^0.1.81", + "blessed-contrib": "^4.8.21", "colors": "^1.4.0", "express": "^4.17.1", "minimist": "^1.2.5", "morgan": "^1.10.0", - "typescript": "^4.2.3" + "typescript": "^4.2.3", + "world-countries": "^4.0.0" } } diff --git a/src/api.ts b/src/api.ts index 50cb242..4304a84 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,6 @@ import express from "express"; import morgan from "morgan"; +import { dashboardRouter } from "./api/dashboardRouter"; import { errorHandler } from "./api/errorHandler"; import { plainRouter } from "./api/plainRouter"; import { regularRouter } from "./api/regularRouter"; @@ -10,8 +11,12 @@ const port = parseInt(process.env.PORT!) || 7070; const app = express(); app.use(morgan("common")); +app.use("/history/web/charts", dashboardRouter); + app.use(userAgentMiddleware); +app.use("/history/charts", dashboardRouter); + /** * Plain CMD/Basic routes have both quiet and full modes * Same with regular / routes with ansi color codes diff --git a/src/api/dashboardRouter.ts b/src/api/dashboardRouter.ts new file mode 100644 index 0000000..7f78667 --- /dev/null +++ b/src/api/dashboardRouter.ts @@ -0,0 +1,54 @@ +import { Request, Router } from "express"; +import { + globalDashboard, + DashboardSize, + countryDashboard, +} from "../utils/routes/dashboard/dashboardHandlers"; +import handleAsync from "./handleAsync"; +import { isTerminal } from "./userAgent"; + +export const dashboardRouter = Router({ mergeParams: true }); + +/** + * + * @param req Express request + * @returns True if the request is from a not from wget, curl or httpie + */ +const isWeb: (req: Request) => boolean = (req) => { + // Check if the link is asking for web version of dashboard + const link = req.baseUrl.startsWith("/history/web/charts"); + // Check if the user agent is NOT coming from terminal based application + const isNotTerminal = !isTerminal(req.headers["user-agent"]); + return link && isNotTerminal; +}; + +dashboardRouter.get( + "/:size?", + handleAsync(async (req, res, next) => { + // Get parameters from request + let size = req.params.size as DashboardSize; + + // Set default size and check then check if size var matches + if (size === undefined) size = "sm"; + if (!["sm", "md", "lg"].includes(size)) return next(); + + let response = await globalDashboard(size, isWeb(req)); + res.send(response); + }) +); + +dashboardRouter.get( + "/:country/:size?", + handleAsync(async (req, res, next) => { + // Get parameters from request + let country = req.params.country; + let size = req.params.size as DashboardSize; + + // Set default size and check then check if size var matches + if (size === undefined) size = "sm"; + if (!["sm", "md", "lg"].includes(size)) return next(); + + let response = await countryDashboard(country, size, isWeb(req)); + res.send(response); + }) +); diff --git a/src/api/userAgent.ts b/src/api/userAgent.ts index 6347feb..358846e 100644 --- a/src/api/userAgent.ts +++ b/src/api/userAgent.ts @@ -13,7 +13,9 @@ export type Handler = ( * @param userAgent The user agent of the requester * @returns A boolean that is true of the user agent provided is from curl / wget / httpie */ -const isTerminal: (userAgent: string | undefined) => boolean = (userAgent) => { +export const isTerminal: (userAgent: string | undefined) => boolean = ( + userAgent +) => { if (userAgent === undefined) return false; if (/curl|wget|httpie/i.test(userAgent)) return true; return false; diff --git a/src/utils/getInformation.ts b/src/utils/getInformation.ts index 90d9249..188cf1c 100644 --- a/src/utils/getInformation.ts +++ b/src/utils/getInformation.ts @@ -11,6 +11,7 @@ axios.defaults.baseURL = "https://disease.sh/v3/covid-19"; export const getAllInfo: () => Promise<{ updated: number; data: { + active: number; cases: number; deaths: number; recovered: number; @@ -19,13 +20,14 @@ export const getAllInfo: () => Promise<{ }; }> = async () => { let { data: globalData } = await axios.get("/all"); - let { cases, deaths, recovered, updated } = globalData; + let { cases, deaths, recovered, updated, active } = globalData; let deathRate = (deaths / cases) * 100; let recoveryRate = (recovered / cases) * 100; return { updated, data: { + active, cases, deaths, recovered, @@ -131,9 +133,10 @@ export async function getHistorical( const dates = Object.keys(mode === "all" ? chartData["cases"] : chartData); // Label for chart - const date = `${ - mode.charAt(0).toUpperCase() + mode.slice(1) - } from ${dates.shift()} to ${dates.pop()}`; + const informationType = + mode === "all" ? "Data" : mode.charAt(0).toUpperCase() + mode.slice(1); + + const date = `${informationType} from ${dates.shift()} to ${dates.pop()}`; if (mode === "all") { return { diff --git a/src/utils/libs/columnizeData.ts b/src/utils/libs/columnizeData.ts new file mode 100644 index 0000000..c1a3112 --- /dev/null +++ b/src/utils/libs/columnizeData.ts @@ -0,0 +1,40 @@ +/** + * + * @param data An object containing your keys and values + * @returns A 2 row column containing your keys and values + */ +export const columnizeData: ( + data: { [key: string]: string }, + padding?: number +) => string = (data, padding) => { + // Generate table + let table = ""; + + // Create columns + let normalizedArray: string[] = []; + Object.keys(data).forEach((key) => { + let value = data[key]; + let line = `${key.padEnd(15, " ")}| ${value.padEnd(13, " ")}`; // create a line with length 30; + normalizedArray.push(line); + }); + + while (normalizedArray.length > 0) { + let left = normalizedArray.shift(); + let right = normalizedArray.shift(); + + //right may be undefined, so default to empty string + if (right === undefined) right = ""; + + table += `${left}${right}`; + if (normalizedArray.length !== 0) table += `\n`; // do not add whitespace at the end of the table + } + + if (padding !== undefined) { + table = table + .split("\n") + .map((line) => " ".repeat(padding) + line) + .join("\n"); + } + + return table; +}; diff --git a/src/utils/routes/dashboard/blessedConfig.ts b/src/utils/routes/dashboard/blessedConfig.ts new file mode 100644 index 0000000..acd56b4 --- /dev/null +++ b/src/utils/routes/dashboard/blessedConfig.ts @@ -0,0 +1,115 @@ +import { DashboardSize } from "./dashboardHandlers"; + +interface BlessedSizeConfiguration { + // MOCKSTDOUT + // Touching the arguments here will mess up with the screenshot function + // Generally, making the numbers bigger will affect the response string because the output will take up more space + // However, making the numbers smaller is just going to add invisible padding on the bottom and the right + mockStdout: number[]; + screenshot: number[]; + + // position: number[] + // grid.set(a, b, c, d); + // a = starting position across the y axis + // b = starting position across the x axis + // c = span across the y axis + // d = span across the x axis + header: { + position: number[]; + }; + map: { + position: number[]; + }; + table: { + position: number[]; + tableXOffset: number; + }; + bar: { + position: number[]; + barXOffset: number; + }; + donut: { + position: number[]; + }; + line: { + position: number[]; + }; +} + +// To the future person who has to touch this, may god have mercy on your soul +export const blessedConfig: { + [key in DashboardSize]: BlessedSizeConfiguration; +} = { + sm: { + mockStdout: [180, 40], + screenshot: [90, 36], + header: { + position: [0, 0, 4, 4], + }, + map: { + position: [0, 4, 4, 5], + }, + table: { + position: [4, 0, 4, 9], + tableXOffset: 15, + }, + bar: { + position: [8, 0, 5, 5], + barXOffset: 3, + }, + donut: { + position: [8, 5, 5, 4], + }, + line: { + position: [13, 0, 5, 9], + }, + }, + md: { + mockStdout: [340, 50], + screenshot: [180, 34], + header: { + position: [0, 0, 4, 3], + }, + map: { + position: [4, 0, 5, 3], + }, + table: { + position: [0, 3, 4, 6], + tableXOffset: 28, + }, + bar: { + position: [4, 3, 5, 3], + barXOffset: 6, + }, + donut: { + position: [4, 6, 5, 3], + }, + line: { + position: [9, 0, 5, 9], + }, + }, + lg: { + mockStdout: [360, 50], + screenshot: [180, 40], + header: { + position: [0, 0, 2, 9], + }, + map: { + position: [6, 0, 5, 3], + }, + table: { + position: [2, 0, 4, 9], + tableXOffset: 60, + }, + bar: { + position: [6, 3, 5, 3], + barXOffset: 8, + }, + donut: { + position: [6, 6, 5, 3], + }, + line: { + position: [11, 0, 5, 9], + }, + }, +}; diff --git a/src/utils/routes/dashboard/dashboardHandlers.ts b/src/utils/routes/dashboard/dashboardHandlers.ts new file mode 100644 index 0000000..1c06730 --- /dev/null +++ b/src/utils/routes/dashboard/dashboardHandlers.ts @@ -0,0 +1,223 @@ +import world from "world-countries"; +import { + getAllInfo, + getCountryInfo, + getHistorical, +} from "../../getInformation"; +import { capitalizeFirstLetter } from "../../libs/capitalizeFirstLetter"; +import { columnizeData } from "../../libs/columnizeData"; +import { getTimestamp } from "../../libs/getTimestamp"; +import { + convertToPercentage, + normalizeNumbers, +} from "../../libs/numberNormalizers"; +import { + generateDashboardOutput, + LineDataObject, +} from "./generateDashboardOutput"; +import { generateWebDashboard } from "./generateWebDashboard"; +export type DashboardSize = "sm" | "md" | "lg"; + +const convertHistoricalDataToChart: (historical: { + [key: string]: { + [key: string]: number; + }; +}) => LineDataObject[] = (historical) => { + let keys = Object.keys(historical); + let response: LineDataObject[] = []; + let colorPerKey = { + cases: "blue", + deaths: "red", + recovered: "green", + }; + + keys.forEach((key) => { + let values = Object.values(historical[key]); + let labels = values.map((_) => " "); + let title = capitalizeFirstLetter(key); + // @ts-expect-error + let color: string = colorPerKey[key]; + + response.push({ + title, + x: labels, + y: values, + style: { + line: color, + }, + }); + }); + + return response; +}; + +/** + * + * @param country Country that the user requested + * @param size Size that the user requested + */ +export const countryDashboard = async ( + country: string, + size: DashboardSize, + isWeb: boolean +) => { + let { + data, + formalCountryName, + apiCountryName, + updated, + } = await getCountryInfo(country); + + // Make Line data + let { chartData, date: lineLabel } = await getHistorical( + "all", + apiCountryName + ); + let convertedHistoricalData = convertHistoricalDataToChart(chartData); + + // Parse data + // prettier-ignore + let { active, cases, casesPerOneMillion, critical, deathRate, deaths, recovered, recoveryRate, todayCases, todayDeaths } = data; + let [deathRatePercent, recoveryRatePercent] = convertToPercentage( + deathRate, + recoveryRate + ); + + // prettier-ignore + let [stringCases, stringDeaths, stringRecovered, stringActive, stringCasesPerMillion, stringTodayCases, stringTodayDeaths, stringCritical + ] = normalizeNumbers(cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical); + + // Make table for dashboard + let tableLabel = getTimestamp(updated); + let tableData = + "\n" + + columnizeData({ + Cases: stringCases, + Deaths: stringDeaths, + Recovered: stringRecovered, + Active: stringActive, + "Cases/Million": stringCasesPerMillion, + "Today Cases": stringTodayCases, + "Today Deaths": stringTodayDeaths, + Critical: stringCritical, + "Mortality %": deathRatePercent, + "Recovery %": recoveryRatePercent, + }); + + const donutData = [ + { + percent: Math.round(deathRate * 1e2) / 1e2, + label: "Mortality %", + color: "red", + }, + { + percent: Math.round(recoveryRate * 1e2) / 1e2, + label: "Recovery %", + color: "green", + }, + ]; + + const barData = { + titles: ["Cases", "Deaths", "Recovered", "Active"], + data: [cases, deaths, recovered, active], + }; + + const countryInfo = world.filter( + (country) => country.name.common === formalCountryName + )[0]; + const [latitude, longitude] = countryInfo.latlng; + const countryCode = countryInfo.cca2; + + const mapMarker = { + latitude, + longitude, + country: countryCode, + }; + + let response = generateDashboardOutput( + { + lineData: convertedHistoricalData, + lineLabel, + tableData, + tableLabel, + mapMarker, + barData, + donutData, + }, + size + ); + + // generate web based dashboard if request is not from terminal + if (isWeb) response = generateWebDashboard(response); + return response; +}; + +/** + * + * @param size Size that the user requested + */ +export const globalDashboard = async (size: DashboardSize, isWeb: boolean) => { + let { data, updated } = await getAllInfo(); + + // Make line data + let { chartData, date: lineLabel } = await getHistorical("all"); + let convertedHistoricalData = convertHistoricalDataToChart(chartData); + + // Parse data + // prettier-ignore + let {cases, deathRate, deaths, recovered, recoveryRate, active} = data; + let [deathRatePercent, recoveryRatePercent] = convertToPercentage( + deathRate, + recoveryRate + ); + + // prettier-ignore + let [stringCases, stringDeaths, stringRecovered, stringActive] = normalizeNumbers(cases, deaths, recovered, active) + + // Make table for box + let tableLabel = getTimestamp(updated); + let tableData = + "\n" + + columnizeData({ + Cases: stringCases, + Deaths: stringDeaths, + Recovered: stringRecovered, + Active: stringActive, + "Mortality %": deathRatePercent, + "Recovery %": recoveryRatePercent, + }); + + const donutData = [ + { + percent: Math.round(deathRate * 1e2) / 1e2, + label: "Mortality %", + color: "red", + }, + { + percent: Math.round(recoveryRate * 1e2) / 1e2, + label: "Recovery %", + color: "green", + }, + ]; + + const barData = { + titles: ["Cases", "Deaths", "Recovered", "Active"], + data: [cases, deaths, recovered, active], + }; + + let response = generateDashboardOutput( + { + lineData: convertedHistoricalData, + lineLabel, + tableData, + tableLabel, + barData, + donutData, + }, + size + ); + + // generate web based dashboard if request is not from terminal + if (isWeb) response = generateWebDashboard(response); + return response; +}; diff --git a/src/utils/routes/dashboard/generateDashboardOutput.ts b/src/utils/routes/dashboard/generateDashboardOutput.ts new file mode 100644 index 0000000..2efe37b --- /dev/null +++ b/src/utils/routes/dashboard/generateDashboardOutput.ts @@ -0,0 +1,204 @@ +import blessed from "blessed"; +import contrib from "blessed-contrib"; +import { welcomeMessage } from "../../libs/getResponses"; +import { blessedConfig } from "./blessedConfig"; +import { DashboardSize } from "./dashboardHandlers"; + +export interface LineDataObject { + title: string; + x: string[]; + y: number[]; + style: { line: string }; +} + +export interface DashboardFunctionInput { + lineData: LineDataObject[]; + lineLabel: string; + tableData: string; + tableLabel: string; + mapMarker?: { + latitude: number; + longitude: number; + country: string; + }; + donutData: { percent: number; label: string; color: string }[]; + barData: { titles: string[]; data: number[] }; +} + +class MockStdout { + constructor(cols: number, rows: number) { + this.columns = cols; + this.rows = rows; + } + isTTY = true; + columns: number; + rows: number; + write = () => {}; + on = () => {}; + removeListener = () => {}; +} + +export const generateDashboardOutput: ( + options: DashboardFunctionInput, + size: DashboardSize +) => string = ( + { + lineData, + lineLabel, + tableData, + tableLabel, + mapMarker, + barData, + donutData, + }, + size +) => { + const sizeConfig = blessedConfig[size]; + + // Generate fake stdout + const [mockX, mockY] = sizeConfig.mockStdout; + const fakeStdout = new MockStdout(mockX, mockY); + + // We specify blessed to use a mock stdout otherwise it is going to render on process.stdout, which we don't want + // We don't actually want blessed to render anything to the string since we want to use it to make graphics that will be rendered seperately + // @ts-expect-error + const screen = blessed.screen({ output: fakeStdout }); + + // Touching the options here will also mess up with the screenshot function + // Generally, making the numbers bigger will affect the response string because the stdout will take up more space + // However, making the numbers smaller is just going to add invisible padding on the bottom and the right + // + // The values here control how much zoom blessed applies when rendering + // Bigger numbers mean that blessed will be zoomed out more + // while smaller numbers will be more zoom edin + // + // The more cols is bigger than rows, the more horizontally squished the output will become + // The more rows is bigger than cols, the more vertically squished the output will become + var grid = new contrib.grid({ cols: 18, rows: 20, screen }); + + // Header box + const [ + headerStartY, + headerStartX, + headerSpanY, + headerSpanX, + ] = sizeConfig.header.position; + grid.set( + headerStartY, + headerStartX, + headerSpanY, + headerSpanX, + blessed.box, + { + content: + welcomeMessage + + ". A curl-based command line tracker for the COVID-19 pandemic.", + } + ); + + // Map box + const [mapStartY, mapStartX, mapSpanY, mapSpanX] = sizeConfig.map.position; + let map = grid.set(mapStartY, mapStartX, mapSpanY, mapSpanX, contrib.map, { + label: "World Map", + }); + if (mapMarker !== undefined) { + let { country, latitude, longitude } = mapMarker; + // @ts-ignore broken typings from blessed devs + map.addMarker({ + lon: longitude, + lat: latitude, + color: "red", + char: `X ${country}`, + }); + } + + // Table box + const [ + tableStartY, + tableStartX, + tableSpanY, + tableSpanX, + ] = sizeConfig.table.position; + const tableXOffset = sizeConfig.table.tableXOffset; + + // Add padding to the table + tableData = tableData + .split("\n") + .map((line) => " ".repeat(tableXOffset) + line) + .join("\n"); + + grid.set(tableStartY, tableStartX, tableSpanY, tableSpanX, blessed.box, { + content: tableData, + label: tableLabel, + }); + + // Bars box + const [barStartY, barStartX, barSpanY, barSpanX] = sizeConfig.bar.position; + + let bar = grid.set(barStartY, barStartX, barSpanY, barSpanX, contrib.bar, { + label: "Information", + barWidth: 8, + barSpacing: 8, + xOffset: sizeConfig.bar.barXOffset, + maxHeight: 9, + }); + screen.append(bar); + bar.setData(barData); + + // Donuts box + const [ + donutStartY, + donutStartX, + donutSpanY, + donutSpanX, + ] = sizeConfig.donut.position; + + let donut = grid.set( + donutStartY, + donutStartX, + donutSpanY, + donutSpanX, + contrib.donut, + { + label: "Percentages", + radius: 8, + arcWidth: 3, + remainColor: "black", + yPadding: 2, + } + ); + donut.setData(donutData); + + // Line + const [ + lineStartY, + lineStartX, + lineSpanY, + lineSpanX, + ] = sizeConfig.line.position; + + let line = grid.set( + lineStartY, + lineStartX, + lineSpanY, + lineSpanX, + contrib.line, + { + xLabelPadding: 3, + xPadding: 5, + showLegend: true, + wholeNumbersOnly: false, + label: lineLabel, + } + ); + line.setData(lineData); + + screen.render(); + + // Take a screenshot + const [screenshotX, screenshotY] = sizeConfig.screenshot; + const response = screen.screenshot(0, screenshotX, 0, screenshotY); + screen.destroy(); + + return response; +}; diff --git a/src/utils/routes/dashboard/generateWebDashboard.ts b/src/utils/routes/dashboard/generateWebDashboard.ts new file mode 100644 index 0000000..5e2388d --- /dev/null +++ b/src/utils/routes/dashboard/generateWebDashboard.ts @@ -0,0 +1,33 @@ +export const generateWebDashboard: (data: string) => string = (data) => { + data = data.replace(/\n/g, "\\r\\n"); + let response = ` + + + + + + +
+ + + + `; + + return response; +}; diff --git a/src/utils/routes/plain/generatePlainOutput.ts b/src/utils/routes/plain/generatePlainOutput.ts index 9d44588..c152eff 100644 --- a/src/utils/routes/plain/generatePlainOutput.ts +++ b/src/utils/routes/plain/generatePlainOutput.ts @@ -1,3 +1,4 @@ +import { columnizeData } from "../../libs/columnizeData"; import { lines } from "../../libs/getResponses"; import { getSaying } from "../../libs/getSaying"; import { getTimestamp } from "../../libs/getTimestamp"; @@ -28,27 +29,7 @@ export const generatePlainOutput: ( let timestamp = getTimestamp(timeUpdated); let saying = getSaying(); - // Generate table - let table = ""; - - // Create columns - let normalizedArray: string[] = []; - Object.keys(data).forEach((key) => { - let value = data[key]; - let line = `${key.padEnd(15, " ")}| ${value.padEnd(13, " ")}`; // create a line with length 30; - normalizedArray.push(line); - }); - - while (normalizedArray.length > 0) { - let left = normalizedArray.shift(); - let right = normalizedArray.shift(); - - //right may be undefined, so default to empty string - if (right === undefined) right = ""; - - table += `${left}${right}`; - if (normalizedArray.length !== 0) table += `\n`; // do not add whitespace at the end of the table - } + let table = columnizeData(data); // responseArray is the array of the raw data **before** adding the separator lines let responseArray: string[] = [timestamp, table]; diff --git a/yarn.lock b/yarn.lock index f27975d..e1620e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,13 @@ resolved "https://registry.yarnpkg.com/@types/asciichart/-/asciichart-1.5.4.tgz#f45cc1615cb8f2689ce68869a87edf1fd3e59b9f" integrity sha512-rUKnjq2MPGwGy4LLw2q2LLcGQloAKBwBN0wBRVvaiZjv/dJoeZYrnLkglqfuqkWwvws0xTS8m8+L6ExqaqqaEA== +"@types/blessed@^0.1.17": + version "0.1.17" + resolved "https://registry.yarnpkg.com/@types/blessed/-/blessed-0.1.17.tgz#15b3280a6b8729f3c270a762ccafc8514ac5120d" + integrity sha512-BKvUtnrXksNdK0fOYV/9HJGkjCcAvOGMSCJsiHaBFyBeyqHwy2OHK32r5XNI+q0eXuAuGqtPOnDetHnbZoYqag== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -113,6 +120,18 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" +ansi-escapes@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" @@ -123,6 +142,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -130,6 +154,18 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-term@>=0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/ansi-term/-/ansi-term-0.0.2.tgz#fd753efa4beada0eac99981bc52a3f6ff019deb7" + integrity sha1-/XU++kvq2g6smZgbxSo/b/AZ3rc= + dependencies: + x256 ">=0.0.1" + +ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= + anymatch@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" @@ -177,6 +213,31 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +blessed-contrib@^4.8.21: + version "4.8.21" + resolved "https://registry.yarnpkg.com/blessed-contrib/-/blessed-contrib-4.8.21.tgz#47c3df41dd5bd35e09dada830d41c38ff3d1cc97" + integrity sha512-zdQMO3UyfhdG0S+Zqn7YexI6BHlnO26qxBKc9jzgyZdIoq7nELiGM/V89CvrTPblZGQr3eMUqCy7pxlKm3vCYA== + dependencies: + ansi-term ">=0.0.2" + chalk "^1.1.0" + drawille-canvas-blessed-contrib ">=0.1.3" + lodash "~>=4.17.11" + map-canvas ">=0.1.5" + marked "^0.7.0" + marked-terminal "^4.0.0" + memory-streams "^0.1.0" + memorystream "^0.3.1" + picture-tuber "^1.0.1" + sparkline "^0.1.1" + strip-ansi "^3.0.0" + term-canvas "0.0.5" + x256 ">=0.0.1" + +blessed@^0.1.81: + version "0.1.81" + resolved "https://registry.yarnpkg.com/blessed/-/blessed-0.1.81.tgz#f962d687ec2c369570ae71af843256e6d0ca1129" + integrity sha1-+WLWh+wsNpVwrnGvhDJW5tDKESk= + body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -222,11 +283,21 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" +bresenham@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/bresenham/-/bresenham-0.0.3.tgz#abdab9e5b194e27c757cd314d8444314f299877a" + integrity sha1-q9q55bGU4nx1fNMU2ERDFPKZh3o= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -250,6 +321,25 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +cardinal@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" + integrity sha1-fMEFXYItISlU0HsIXeolHMe8VQU= + dependencies: + ansicolors "~0.3.2" + redeyed "~2.1.0" + +chalk@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -258,6 +348,19 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +charm@~0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/charm/-/charm-0.1.2.tgz#06c21eed1a1b06aeb67553cdc53e23274bac2296" + integrity sha1-BsIe7RobBq62dVPNxT4jJ0usIpY= + chokidar@^3.2.2: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" @@ -283,6 +386,13 @@ cli-boxes@^2.2.0: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +cli-table@^0.3.1: + version "0.3.6" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" + integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== + dependencies: + colors "1.0.3" + clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -302,6 +412,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= + colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -346,6 +461,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -414,6 +534,22 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +drawille-blessed-contrib@>=0.0.1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/drawille-blessed-contrib/-/drawille-blessed-contrib-1.0.0.tgz#15c27934f57a0056ad13596e1561637bc941f0b7" + integrity sha1-FcJ5NPV6AFatE1luFWFje8lB8Lc= + +drawille-canvas-blessed-contrib@>=0.0.1, drawille-canvas-blessed-contrib@>=0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/drawille-canvas-blessed-contrib/-/drawille-canvas-blessed-contrib-0.1.3.tgz#212f078a722bfd2ecc267ea86ab6dddc1081fd48" + integrity sha1-IS8HinIr/S7MJn6oarbd3BCB/Ug= + dependencies: + ansi-term ">=0.0.2" + bresenham "0.0.3" + drawille-blessed-contrib ">=0.0.1" + gl-matrix "^2.1.0" + x256 ">=0.0.1" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -456,11 +592,28 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-string-regexp@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-stream@~0.9.8: + version "0.9.8" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-0.9.8.tgz#5da9cf3c7900975989db5a68c28e5b3c98ebe03a" + integrity sha1-XanPPHkAl1mJ21powo5bPJjr4Do= + dependencies: + optimist "0.2" + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -551,6 +704,11 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" +gl-matrix@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-2.8.1.tgz#1c7873448eac61d2cd25803a074e837bd42581a3" + integrity sha512-0YCjVpE3pS5XWlN3J4X7AiAx65+nqAI54LndtVFnQZB6G/FVLkZH8y8V6R3cIoOQR4pUdfwQGd1iwyoXHJ4Qfw== + glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -587,6 +745,13 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -602,6 +767,11 @@ has-yarn@^2.1.0: resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== +here@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/here/-/here-0.0.2.tgz#69c1af3f02121f3d8788e02e84dc8b3905d71195" + integrity sha1-acGvPwISHz2HiOAuhNyLOQXXEZU= + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -656,7 +826,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4: +inherits@2.0.4, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -750,6 +920,11 @@ is-yarn-global@^0.3.0: resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -769,6 +944,16 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= + +lodash@~>=4.17.11: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -791,11 +976,48 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +map-canvas@>=0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/map-canvas/-/map-canvas-0.1.5.tgz#8be6bade0bf3e9f9a8b56e8836a1d1d133cab186" + integrity sha1-i+a63gvz6fmotW6INqHR0TPKsYY= + dependencies: + drawille-canvas-blessed-contrib ">=0.0.1" + xml2js "^0.4.5" + +marked-terminal@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-4.1.1.tgz#34a6f063cd6cfe26bffaf5bac3724e24242168a9" + integrity sha512-t7Mdf6T3PvOEyN01c3tYxDzhyKZ8xnkp8Rs6Fohno63L/0pFTJ5Qtwto2AQVuDtbQiWzD+4E5AAu1Z2iLc8miQ== + dependencies: + ansi-escapes "^4.3.1" + cardinal "^2.1.1" + chalk "^4.1.0" + cli-table "^0.3.1" + node-emoji "^1.10.0" + supports-hyperlinks "^2.1.0" + +marked@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e" + integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +memory-streams@^0.1.0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/memory-streams/-/memory-streams-0.1.3.tgz#d9b0017b4b87f1d92f55f2745c9caacb1dc93ceb" + integrity sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA== + dependencies: + readable-stream "~1.0.2" + +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -871,6 +1093,13 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +node-emoji@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" + integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== + dependencies: + lodash.toarray "^4.4.0" + nodemon@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.7.tgz#6f030a0a0ebe3ea1ba2a38f71bf9bab4841ced32" @@ -894,6 +1123,13 @@ nopt@~1.0.10: dependencies: abbrev "1" +nopt@~2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af" + integrity sha1-bMzZd7gBMqB3MdbozljCyDA8+a8= + dependencies: + abbrev "1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -923,6 +1159,20 @@ once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +optimist@0.2: + version "0.2.8" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.2.8.tgz#e981ab7e268b457948593b55674c099a815cac31" + integrity sha1-6YGrfiaLRXlIWTtVZ0wJmoFcrDE= + dependencies: + wordwrap ">=0.0.1 <0.1.0" + +optimist@~0.3.4: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + integrity sha1-yQlBrVnkJzMokjB00s8ufLxuwNk= + dependencies: + wordwrap "~0.0.2" + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -953,6 +1203,23 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picture-tuber@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/picture-tuber/-/picture-tuber-1.0.2.tgz#2f6f024a882fbd28869d0b78a8d1ab45950e6cbf" + integrity sha512-49/xq+wzbwDeI32aPvwQJldM8pr7dKDRuR76IjztrkmiCkAQDaWFJzkmfVqCHmt/iFoPFhHmI9L0oKhthrTOQw== + dependencies: + buffers "~0.1.1" + charm "~0.1.0" + event-stream "~0.9.8" + optimist "~0.3.4" + png-js "~0.1.0" + x256 "~0.0.1" + +png-js@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/png-js/-/png-js-0.1.1.tgz#1cc7c212303acabe74263ec3ac78009580242d93" + integrity sha1-HMfCEjA6yr50Jj7DrHgAlYAkLZM= + prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" @@ -1016,6 +1283,16 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +readable-stream@~1.0.2: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -1023,6 +1300,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +redeyed@~2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" + integrity sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs= + dependencies: + esprima "~4.0.0" + registry-auth-token@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" @@ -1054,6 +1338,11 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -1123,6 +1412,14 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sparkline@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/sparkline/-/sparkline-0.1.2.tgz#c3bde46252b1354e710c4b200d54816bd9f07a32" + integrity sha1-w73kYlKxNU5xDEsgDVSBa9nwejI= + dependencies: + here "0.0.2" + nopt "~2.1.2" + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -1146,6 +1443,18 @@ string-width@^4.0.0, string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + strip-ansi@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -1165,6 +1474,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1172,13 +1486,26 @@ supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-hyperlinks@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +term-canvas@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/term-canvas/-/term-canvas-0.0.5.tgz#597afac2fa6369a6f17860bce9c5f66d6ea0ca96" + integrity sha1-WXr6wvpjaabxeGC86cX2bW6gypY= + term-size@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" @@ -1220,6 +1547,11 @@ ts-node@^9.1.1: source-map-support "^0.5.17" yn "3.1.1" +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -1307,6 +1639,16 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +"wordwrap@>=0.0.1 <0.1.0", wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +world-countries@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/world-countries/-/world-countries-4.0.0.tgz#a14757e2240d8c16249e3c6e6cdcd61d119a042d" + integrity sha512-LsFFYmggquj0U+i7VUaJOZYz5F4QNu+oceGw8odnyVHMT2LxYSdVncqdouOEnq1esr7yCakp9+3BZTztuSw1Pg== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -1322,11 +1664,29 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +x256@>=0.0.1, x256@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/x256/-/x256-0.0.2.tgz#c9af18876f7a175801d564fe70ad9e8317784934" + integrity sha1-ya8Yh296F1gB1WT+cK2egxd4STQ= + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xml2js@^0.4.5: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" From 0f05b5d6640e8844ac2749bb0494ff04df1cb899 Mon Sep 17 00:00:00 2001 From: scinorandex Date: Mon, 12 Apr 2021 11:46:58 +0800 Subject: [PATCH 4/5] Add function to remove garbage on web dashboard --- src/utils/libs/generateTable.ts | 2 +- src/utils/routes/dashboard/blessedConfig.ts | 6 +-- .../dashboard/generateDashboardOutput.ts | 40 +++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/utils/libs/generateTable.ts b/src/utils/libs/generateTable.ts index 8b468a4..4ae5d89 100644 --- a/src/utils/libs/generateTable.ts +++ b/src/utils/libs/generateTable.ts @@ -1,7 +1,7 @@ import colors from "colors"; import { BoxArt, getBoxArt } from "./getBoxArt"; -const removeANSI: (str: string) => string = (str) => { +export const removeANSI: (str: string) => string = (str) => { while (str.includes("\x1B")) { str = str.replace(/\u001b[^m]*?m/g, ""); } diff --git a/src/utils/routes/dashboard/blessedConfig.ts b/src/utils/routes/dashboard/blessedConfig.ts index acd56b4..0af0368 100644 --- a/src/utils/routes/dashboard/blessedConfig.ts +++ b/src/utils/routes/dashboard/blessedConfig.ts @@ -55,7 +55,7 @@ export const blessedConfig: { }, bar: { position: [8, 0, 5, 5], - barXOffset: 3, + barXOffset: 1, }, donut: { position: [8, 5, 5, 4], @@ -79,7 +79,7 @@ export const blessedConfig: { }, bar: { position: [4, 3, 5, 3], - barXOffset: 6, + barXOffset: 4, }, donut: { position: [4, 6, 5, 3], @@ -103,7 +103,7 @@ export const blessedConfig: { }, bar: { position: [6, 3, 5, 3], - barXOffset: 8, + barXOffset: 5, }, donut: { position: [6, 6, 5, 3], diff --git a/src/utils/routes/dashboard/generateDashboardOutput.ts b/src/utils/routes/dashboard/generateDashboardOutput.ts index 2efe37b..4f4a8ea 100644 --- a/src/utils/routes/dashboard/generateDashboardOutput.ts +++ b/src/utils/routes/dashboard/generateDashboardOutput.ts @@ -1,5 +1,6 @@ import blessed from "blessed"; import contrib from "blessed-contrib"; +import { removeANSI } from "../../libs/generateTable"; import { welcomeMessage } from "../../libs/getResponses"; import { blessedConfig } from "./blessedConfig"; import { DashboardSize } from "./dashboardHandlers"; @@ -137,8 +138,8 @@ export const generateDashboardOutput: ( let bar = grid.set(barStartY, barStartX, barSpanY, barSpanX, contrib.bar, { label: "Information", - barWidth: 8, - barSpacing: 8, + barWidth: 9, + barSpacing: 9, xOffset: sizeConfig.bar.barXOffset, maxHeight: 9, }); @@ -197,8 +198,41 @@ export const generateDashboardOutput: ( // Take a screenshot const [screenshotX, screenshotY] = sizeConfig.screenshot; - const response = screen.screenshot(0, screenshotX, 0, screenshotY); + let response = screen.screenshot(0, screenshotX, 0, screenshotY); screen.destroy(); + response = removeUnneededLines(response); return response; }; + +const removeUnneededLines: (str: string) => string = (str) => { + // Split the input + let splitLines = str.split("\n"); + + // Lines with ansi removed + let rawLines = splitLines.map((line) => { + // If line contains a background color code then replace it with NON ansi string + // This is mostly to preserve bars since they are just whitespace + if (line.includes("\x1B[4")) line.replace("\x1B[4", "_"); + return removeANSI(line); + }); + + // This array represents the indexes of the lines in splitLines that are good and should be kept + let goodLines: number[] = []; + + rawLines.forEach((line, index) => { + // remove border + line = line.replace(/│/g, ""); + + // remove spaces + line = line.replace(/\s/g, ""); + if (line.length !== 0) goodLines.push(index); + }); + + let response: string[] = []; + splitLines.forEach((line, index) => { + if (goodLines.includes(index)) response.push(line); + }); + + return response.join("\n"); +}; From cf29faf1396ed6cc4f9419a54647e225115c1888 Mon Sep 17 00:00:00 2001 From: scinorandex Date: Mon, 12 Apr 2021 20:22:49 +0800 Subject: [PATCH 5/5] Add minor changes to dashboard --- src/utils/routes/dashboard/blessedConfig.ts | 4 ++-- .../dashboard/generateDashboardOutput.ts | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/utils/routes/dashboard/blessedConfig.ts b/src/utils/routes/dashboard/blessedConfig.ts index 0af0368..e6191b4 100644 --- a/src/utils/routes/dashboard/blessedConfig.ts +++ b/src/utils/routes/dashboard/blessedConfig.ts @@ -41,8 +41,8 @@ export const blessedConfig: { [key in DashboardSize]: BlessedSizeConfiguration; } = { sm: { - mockStdout: [180, 40], - screenshot: [90, 36], + mockStdout: [180, 44], + screenshot: [90, 40], header: { position: [0, 0, 4, 4], }, diff --git a/src/utils/routes/dashboard/generateDashboardOutput.ts b/src/utils/routes/dashboard/generateDashboardOutput.ts index 4f4a8ea..16b845b 100644 --- a/src/utils/routes/dashboard/generateDashboardOutput.ts +++ b/src/utils/routes/dashboard/generateDashboardOutput.ts @@ -1,7 +1,8 @@ import blessed from "blessed"; import contrib from "blessed-contrib"; import { removeANSI } from "../../libs/generateTable"; -import { welcomeMessage } from "../../libs/getResponses"; +import { lines, welcomeMessage } from "../../libs/getResponses"; +import { getSaying } from "../../libs/getSaying"; import { blessedConfig } from "./blessedConfig"; import { DashboardSize } from "./dashboardHandlers"; @@ -201,7 +202,17 @@ export const generateDashboardOutput: ( let response = screen.screenshot(0, screenshotX, 0, screenshotY); screen.destroy(); + // Remove garbage lines response = removeUnneededLines(response); + response += "\n"; + + response += getSaying() + "\n"; + response += lines.WNrepoLink + "\n\n"; + response += lines.BMCLink + "\n"; + response += lines.sponsorMessage + "\n"; + response += lines.twitterPlug; + response += lines.handleHashtag.join(" ") + "\n"; + return response; }; @@ -212,7 +223,7 @@ const removeUnneededLines: (str: string) => string = (str) => { // Lines with ansi removed let rawLines = splitLines.map((line) => { // If line contains a background color code then replace it with NON ansi string - // This is mostly to preserve bars since they are just whitespace + // This is mostly to preserve bars since they are just colored whitespace if (line.includes("\x1B[4")) line.replace("\x1B[4", "_"); return removeANSI(line); }); @@ -221,11 +232,9 @@ const removeUnneededLines: (str: string) => string = (str) => { let goodLines: number[] = []; rawLines.forEach((line, index) => { - // remove border - line = line.replace(/│/g, ""); + // remove border and spaces + line = line.replace(/│/g, "").replace(/\s/g, ""); - // remove spaces - line = line.replace(/\s/g, ""); if (line.length !== 0) goodLines.push(index); });