diff --git a/.gitignore b/.gitignore index b52a99e..c9da2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +~/ etc/ node_modules/ #trademark reasons. diff --git a/README.md b/README.md new file mode 100644 index 0000000..934754f --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# SpaceX-cli +## View upcoming launches in the terminal + +## Write a readme for this later... + +`npm install -g npc-strider/spacex-cli` + +This is an enhanced version of a basic CLI bash script I was using. + +Because the bash script was based on the v3 [spacexdata](https://github.com/r-spacex/SpaceX-API) api, I was forced to upgrade it to use the new v4 api. + +So I decided to not just upgrade the bash script to use the new api, but also add some new features and more interactibility. + +The cli isn't pretty like some others, but I think it's quite dense in relevant information. + +*I'm not including space.ico in my releases to prevent trademark infringement :/ sorry. \ No newline at end of file diff --git a/constants.js b/constants.js index 4b3e91c..39615c5 100644 --- a/constants.js +++ b/constants.js @@ -1,11 +1,16 @@ +const os = require('os'); const clc = require('cli-color'); const arguments = require('./tools/process-args').arguments; -if (arguments.color) { - this.STYLES = {scrollbar: {bg: 'blue'}} +if (!arguments.dump) { + if (arguments.color) { + this.STYLES = {scrollbar: {bg: 'blue'}}; + } else { + this.STYLES = {scrollbar: {bg: 'white'}}; + }; } else { - this.STYLES = {scrollbar: {bg: 'white'}} + this.STYLES = {scrollbar: null}; }; const override_deep = (o,v) => { @@ -20,14 +25,34 @@ const override_deep = (o,v) => { return o_ }; +this.CONTROL_MAX_WIDTH = 0; +const registerKeys = ((keys, actions) => { + var strArr = []; + for (i = 0; i < keys.length; ++i) { + if (arguments.color) { + strArr.push(keys[i]+' '+clc.underline(actions[i])); + } else { + strArr.push(keys[i]+' '+actions[i]); + }; + }; + const str = strArr.join(' | '); + const d = arguments.color ? 9 : 0; + if (str.length - i*d > this.CONTROL_MAX_WIDTH) { + this.CONTROL_MAX_WIDTH = str.length - i*d; + }; + return str; +}); + this.CONSTANT_VALUES = { - TIME_NOTIFY: 60*60*24*7, //FIXME: TESTING VALUES - Change to 60*90 - TIME_BLINK: 60*60*24*7 //FIXME: TESTING VALUES - Change to 60*60*24 + TIME_NOTIFY: arguments.notify_time,//60*60*24*7, + TIME_BLINK: arguments.highlight_time, + DATA_PATH: arguments.path.replace('~',os.homedir), } this.GETCOLORS = ((clc_) => { var COLOR = { NONE: ((t) => {return t}), + BLINK: clc_.blink, GENERIC: clc_.cyan, SUCCESS: clc_.green, HUGE_SUCCESS: clc_.green.bold, @@ -35,14 +60,14 @@ this.GETCOLORS = ((clc_) => { DANGER: clc_.red.bold, INVALID: clc_.blackBright.bold, PROGRESS: clc_.yellow, - HEADER: clc_.underline, + HEADER: clc_.magentaBright.underline, TIME: { LT1: clc_.green, LTD: clc_.green, LTM: clc_.yellow, LTQ: clc_.blackBright.bold, LTH: clc_.blackBright.bold - } + }, }; if (!arguments.color) { @@ -62,28 +87,49 @@ this.GETSTRING = ((COLOR_) => { PRECISION: COLOR_.HEADER("Precision"), FLAGS: COLOR_.HEADER("Flags"), ROCKET: COLOR_.HEADER("Rocket type"), - CORE: COLOR_.HEADER("Core (№. reused)"), + CORE: COLOR_.HEADER("Core(№ of reuses)"), LAUNCHPAD: COLOR_.HEADER("Launchpad"), LAUNCHPAD_REG: COLOR_.HEADER("Launch region"), PAYLOAD_NAME: COLOR_.HEADER("Payloads"), PAYLOAD_CUSTOMERS: COLOR_.HEADER("Payload customers"), + + JSON: COLOR_.HEADER('Key/Value'), + KEY: COLOR_.HEADER('Property'), + VALUE: COLOR_.HEADER('Value'), }, CONTROLS: { - TABLE: '↑↓ Scroll and select | ↵ → Select launch | r Refresh | q Quit', - INFORMATION: '↑↓ Scroll | q ↵ ← Return to table | j Toggle JSON view' + TABLE: registerKeys(['↑↓', '↵ →', 'r', 'q', 'd' ], + ['Scroll and select', 'Select launch', 'Refresh', 'Quit', 'Diff']), + INFORMATION: registerKeys(['↑↓', 'q ↵ ←', 'j' ], + ['Scroll', 'Return to table', 'Toggle JSON view']), + DIFF: registerKeys(['↑↓', 'q ↵ ←' ], + ['Scroll', 'Return to table']) }, - CORE_UNASSIGNED: COLOR_.INVALID("Unknown"), + CORE_UNASSIGNED: COLOR_.INVALID("Unknown"), + CORE_UNASSIGNED_: "Unknown", DOWNLOADING: COLOR_.PROGRESS("Downloading new data from spacex api . . .",), DOWNLOADING_STAR: COLOR_.PROGRESS("*"), OK_GENERIC: COLOR_.SUCCESS("OK."), - OK_SCROLL: COLOR_.SUCCESS("OK. [!] Warning: Scroll down to view more"), + OK_SCROLL: COLOR_.SUCCESS("OK. [!] Scroll to view more"), + LAST_DELTA: COLOR_.GENERIC('Last Δ: '), H_WARNING: "SpaceX launches imminent!", + NEW_DATA: "New SpaceX data downloaded!", APPID: 'spacex-cli', SCREEN_TITLE: 'Spacex - Upcoming lunches', + + DIFF: { + PREVIOUS: 'prev', + CURRENT: 'curr', + PREVIOUS_SYMBOL: '[-]', + CURRENT_SYMBOL: '[+]', + BOTH_SYMBOL: '[±]', + UNCHANGED_SYMBOL: '[=]', + UNCHANGED_SYMBOL_: ' ' + } }; }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3042124..c5c0d0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,11 @@ "type": "^1.0.1" } }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, "es5-ext": { "version": "0.10.53", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", diff --git a/package.json b/package.json index 939fef1..6b6cd98 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "spacex-cli", - "version": "0.1.0", + "version": "1.0.0", "description": "SpaceX upcoming launch tracker", "main": "spacex-cli.js", "scripts": { "test": "node spacex-cli" }, + "bin": { + "spacex-cli": "spacex-cli" + }, "keywords": [ "spacex", "cli" @@ -14,6 +17,7 @@ "license": "GPL-3.0", "dependencies": { "cli-color": "^2.0.0", + "diff": "^5.0.0", "mri": "^1.1.6", "neo-blessed": "^0.2.0", "node-fetch": "^2.6.1", diff --git a/spacex-cli.js b/spacex-cli.js index 00abc12..5c2e1cd 100644 --- a/spacex-cli.js +++ b/spacex-cli.js @@ -1,10 +1,11 @@ +#!/usr/bin/env node const clc = require('cli-color'); const notifier = require('node-notifier'); -const blessed = require('neo-blessed'); +const Diff = require('diff'); const path = require('path'); const arguments = require('./tools/process-args').arguments; -const cli_elem = require('./tools/cli-elements'); +const cli_elem = require('./tools/cli-elements'); //Idk js package structure/conventions... const date_tools = require('./tools/date-tools'); const net_tools = require('./tools/net-tools'); const format_tools = require('./tools/format-tools'); @@ -16,6 +17,8 @@ const CONSTANT_VALUES = constants.CONSTANT_VALUES; var screen = cli_elem.screen; var table_element = cli_elem.table_element; +var diff_table_element = cli_elem.diff_table_element; +var diff_json_element = cli_elem.diff_json_element; var status_element = cli_elem.status_element; var countdown_element = cli_elem.countdown_element; var controls_element = cli_elem.controls_element; @@ -30,7 +33,6 @@ const prettyPrintData = format_tools.prettyPrintData; // ===================================== -//TODO: archive data option //TODO: better viewer (than json and the crappy one I made using only string manipulation.) process.env.FORCE_COLOR = true; @@ -41,12 +43,14 @@ const SCREEN_REFRESH = arguments.screen_refresh; //ms const API_REFRESH = arguments.api_refresh; //ms const API_REFRESH_CYCLES = API_REFRESH/SCREEN_REFRESH; const NONINTERACTIVE = arguments.dump; +const BLINK = arguments.blink; +const ARCHIVE = arguments.archive; // var notified_1h = []; var notifying_1h = []; -async function formatData(arr) { +function selectData(arr) { const LUNCHPADS_RESP = arr[0]; const LUNCHES_RESP = arr[1]; const ROCKETS_RESP = arr[2]; @@ -95,10 +99,13 @@ async function formatData(arr) { payload_customers_str = payload_customers_str.substr(0, payload_customers_str.length-3) var cores_str = ""; + var cores_str_ = ""; lunch.cores.forEach(core_ => { cores_str += CORES[core_["core"]] ? (CORES[core_["core"]]["serial"] + '(' + CORES[core_["core"]]["reuse_count"] + ') ') : STRING.CORE_UNASSIGNED + ' '; + cores_str_ += CORES[core_["core"]] ? (CORES[core_["core"]]["serial"] + '(' + CORES[core_["core"]]["reuse_count"] + ') ') : STRING.CORE_UNASSIGNED_ + ' '; }); - cores_str = cores_str.substr(0, cores_str.length-1) + cores_str = cores_str.substr(0, cores_str.length-1); + cores_str_ = cores_str_.substr(0, cores_str_.length-1); var time_style = { hour: { p:"minutes", c: COLOR.TIME.LT1 }, @@ -108,82 +115,70 @@ async function formatData(arr) { half: { p:"days", c: COLOR.TIME.LTH } }[precision] const dt_s = t_s - new Date().getTime() / 1000; - var dt_str = secondsHumanReadable(dt_s,time_style.p); - if (dt_s <= CONSTANT_VALUES.TIME_NOTIFY && precision == "hour" && !lunch.tbd && !lunch.net && !notified_1h.includes(lunch.flight_number)) { //Notify user. + const dt_str_ = secondsHumanReadable(dt_s,time_style.p); + var dt_str; + if (dt_s <= CONSTANT_VALUES.TIME_NOTIFY && precision === "hour" && !lunch.tbd && !lunch.net && !notified_1h.includes(lunch.flight_number)) { //Notify user. notified_1h.push(lunch.flight_number) notifying_1h.push(lunch.name) } - if (dt_s <= CONSTANT_VALUES.TIME_BLINK && precision == "hour" && !lunch.tbd && !lunch.net) { - dt_str = COLOR.HUGE_SUCCESS("! "+clc.blink(dt_str))+COLOR.HUGE_SUCCESS(" !"); + if (dt_s <= CONSTANT_VALUES.TIME_BLINK && precision === "hour" && !lunch.tbd && !lunch.net) { + dt_str = COLOR.HUGE_SUCCESS("! "+(BLINK ? COLOR.BLINK(dt_str_) : dt_str_))+COLOR.HUGE_SUCCESS(" !"); date_h = COLOR.HUGE_SUCCESS(date_arr.bw.join('')); } else if (dt_s < 0) { - dt_str = COLOR.INVALID(dt_str); + dt_str = COLOR.INVALID(dt_str_); date_h = COLOR.INVALID(date_arr.bw.join(''))+'{/}'; } else { - dt_str = time_style.c(dt_str) + dt_str = time_style.c(dt_str_) date_h = date_arr.col.join(''); } + const precision_ = precision; precision = time_style.c(precision) return { name: lunch.name, - flight_number: lunch.flight_number, + flight_number: String(lunch.flight_number), date_precision: precision, + date_precision_:precision_, tbd: lunch.tbd, net: lunch.net, date_h: date_h, + date_h_: date_arr.bw.join(''), rocket: ROCKETS[lunch.rocket], cores: cores_str, + cores_: cores_str_, launchpad: lunchpad.name, launchpad_reg: lunchpad.region, dt: dt_str, + dt_: dt_str_, payloads: { names_str: payload_names_str, customers_str: payload_customers_str } } }); + return LUNCHES; +}; +async function formatData(arr) { + return format_tools.tabularizeData(selectData(arr)); +}; - LUNCHES = LUNCHES.map(lunch => [ - COLOR.GENERIC(lunch.flight_number), - COLOR.GENERIC(lunch.name), - lunch.date_h, - lunch.dt, - String(lunch.date_precision), - COLOR.DANGER((lunch.tbd ? "tbd" : "")+(lunch.net && lunch.tbd ? ", " : "")+(lunch.net ? "net" : "")), - COLOR.GENERIC(lunch.rocket), - COLOR.GENERIC(lunch.cores), - COLOR.GENERIC(lunch.launchpad), - // lunch.launchpad_reg - COLOR.GENERIC(lunch.payloads.names_str), - COLOR.GENERIC(lunch.payloads.customers_str) - ]) - LUNCHES.unshift([ - STRING.HEADERS.FLIGHT_NUMBER, - STRING.HEADERS.NAME, - STRING.HEADERS.DATE_H, - STRING.HEADERS.DT, - STRING.HEADERS.PRECISION, - STRING.HEADERS.FLAGS, - STRING.HEADERS.ROCKET, - STRING.HEADERS.CORE, - STRING.HEADERS.LAUNCHPAD, - // STRING.HEADERS.LAUNCHPAD_REG - STRING.HEADERS.PAYLOAD_NAME, - STRING.HEADERS.PAYLOAD_CUSTOMERS, - ],[ ' ', ' ', ' ', ' ', ' ', ' ', ' ',' ', ' ', ' ', ' ']) - return LUNCHES -} +var diff_table_content; +var diff_json_content; +var newData = false; var json_view = false; //Keep these keybind listeners in main // Quit on Escape, q, or Control-C. screen.key(['escape', 'q', '', 'C-c', 'left'], (ch, key) => { - if (screen.focused.name == "table" && key.name != 'left') { + const name = screen.focused.name; + if (name === "table" && key.name !== 'left') { console.log('\033[?25h'); return process.exit(0); - } else if (screen.focused.name == "detailed_information") { + } else if (name === "detailed_information" || name === "diff_table" || name === "diff_json") { + if (name === "diff_table" || name === "diff_json") { + newData = false; + }; table_element.setFront(); table_element.focus(); controls_element.setContent(STRING.CONTROLS.TABLE); @@ -191,12 +186,48 @@ screen.key(['escape', 'q', '', 'C-c', 'left'], (ch, key) => { }; }); screen.key(['j'], (ch, key) => { - json_view = (json_view ? false : true); - information_element.setData(prettyPrintData(data_cache, idx, json_view)); + const name = screen.focused.name; + if (name === "detailed_information") { + json_view = (json_view ? false : true); + information_element.setData(prettyPrintData(data_cache, idx, json_view)); + } else if (name === "diff_table") { + json_view = true; + diff_json_element.setFront(); + diff_json_element.focus(); + diff_json_element.enableInput(); + } else if (name === "diff_json") { + json_view = false; + diff_table_element.setFront(); + diff_table_element.focus(); + diff_table_element.enableInput(); + }; + screen.render(); +}); +screen.key(['d'], (ch, key) => { + const name = screen.focused.name; + if (name !== "diff_table" && name !== "diff_json") { + // console.log(diff_table_content); + // process.kill(process.pid) + controls_element.setContent(STRING.CONTROLS.INFORMATION); + if (json_view) { + diff_json_element.setFront(); + diff_json_element.focus(); + diff_json_element.enableInput(); + } else { + diff_table_element.setFront(); + diff_table_element.focus(); + diff_table_element.enableInput(); + }; + } else { + table_element.setFront(); + table_element.focus(); + controls_element.setContent(STRING.CONTROLS.TABLE); + }; screen.render(); }); screen.key(['enter', 'right'], (ch, key) => { - if (screen.focused.name == "table") { + const name = screen.focused.name; + if (name === "table") { idx = table_element.getScroll()-2; if (idx >= 0) { status_element.setContent(String(data_cache[1][idx].name)); @@ -206,7 +237,10 @@ screen.key(['enter', 'right'], (ch, key) => { information_element.setData(prettyPrintData(data_cache, idx, json_view)); controls_element.setContent(STRING.CONTROLS.INFORMATION); }; - } else if (screen.focused.name == "detailed_information") { + } else if (key.name === 'enter' && (name === "detailed_information" || name === "diff_table" || name === "diff_json")) { + if (name === "diff_table" || name === "diff_json") { + newData = false; + }; table_element.setFront(); table_element.focus(); controls_element.setContent(STRING.CONTROLS.TABLE); @@ -218,6 +252,9 @@ screen.key(['r'], (ch, key) => { }); var data_cache; +var checkDiff = true; +var firstRun = true; +var lastDelta = ''; var api_counter = 1; (async function(){ @@ -227,13 +264,16 @@ var api_counter = 1; table_element.select(1); status_element.setContent(STRING.DOWNLOADING); countdown_element.setContent(STRING.DOWNLOADING_STAR); + diff_table_element.setData(diff_table_content ? diff_table_content : [['diff_table_content']]); + screen.render(); })() async function main() { api_counter--; - if (api_counter == 0) { + if (api_counter === 0) { + checkDiff = true; status_element.setContent(STRING.DOWNLOADING); countdown_element.setContent(STRING.DOWNLOADING_STAR); screen.render(); @@ -242,22 +282,22 @@ async function main() { } if (data_cache) { var err_message = await formatErr(await data_cache); - if (err_message == true) { + if (err_message === true) { const TABLE = await formatData(await data_cache); const SCROLL = table_element.getScroll(); table_element.setData(TABLE); table_element.select(SCROLL); - err_message = [( (data_cache[1].length - (table_element.height - 4)) > 0 ? STRING.OK_SCROLL : STRING.OK_GENERIC)]; - if (notifying_1h.length > 0) { - notifier.notify({ - title: STRING.H_WARNING, - message: notifying_1h.join(' │ '), - icon: path.join(__dirname, 'spacex.ico'), - appID: STRING.APPID - }); - notifying_1h = []; - } - } + err_message = [( (data_cache[1].length - (table_element.height - 4)) > 0 ? STRING.OK_SCROLL : STRING.OK_GENERIC) + (newData ? ' | '+COLOR.WARNING(STRING.NEW_DATA) : '')+' '+lastDelta]; + }; + if (notifying_1h.length > 0) { + notifier.notify({ + title: STRING.H_WARNING, + message: notifying_1h.join(' │ '), + icon: path.join(__dirname, 'spacex.ico'), + appID: STRING.APPID + }); + notifying_1h = []; + }; if (NONINTERACTIVE) { screen.remove(status_element); screen.remove(countdown_element); @@ -272,17 +312,88 @@ async function main() { } status_element.setContent(err_message[api_counter%err_message.length]); countdown_element.setContent("Next update in "+secondsHumanReadable(api_counter*SCREEN_REFRESH/1000)); - } + + + if (checkDiff) { + checkDiff = false; + const path_prev_launches = path.join(CONSTANT_VALUES.DATA_PATH,'previous_data.json'); + const path_prev_launches_table = path.join(CONSTANT_VALUES.DATA_PATH,'previous_data_table.json'); + + const curr_launches = data_cache[1]; + var prev_launches = net_tools.readFile(path_prev_launches); + prev_launches = prev_launches !== '' ? JSON.parse(prev_launches) : []; + + diff_json_content = [[STRING.HEADERS.JSON],['']]; + // jsonDiff.diffString(prev_launches, curr_launches) + // .split('\n') + // .forEach(u => {diff_json_content.push([clc.cyan(u)])}); + const json_diff_data = Diff.diffJson(prev_launches, curr_launches) + const changed = json_diff_data.some(chunk => { + return chunk.added || chunk.removed; + }); + + if (changed || firstRun) { + firstRun = false; + + json_diff_data.forEach(chunk => { + if (chunk.added && chunk.removed) {//Not sure if this case is possible. + chunk.value.split('\\n').forEach(line => {[diff_json_content.push([COLOR.WARNING(STRING.DIFF.BOTH_SYMBOL+'│ '+line)])]}); + } else if (chunk.added) { + chunk.value.split('\\n').forEach(line => {[diff_json_content.push([COLOR.SUCCESS(STRING.DIFF.CURRENT_SYMBOL+'│ '+line)])]}); + } else if (chunk.removed) { + chunk.value.split('\\n').forEach(line => {[diff_json_content.push([COLOR.DANGER(STRING.DIFF.PREVIOUS_SYMBOL+'│ '+line)])]}); + } else { + chunk.value.replace(/\n/gm,"_NEWLINE_") //Why the frick doesn't my regexp work when it works in a tester???? Using this workaround. + .replace(/_NEWLINE_\s\s\{(.*?)_NEWLINE_\s\s\},/gm, '_NEWLINE_ [...]') + .replace(/_NEWLINE_\s\s\{(.*?)_NEWLINE_]/m, '_NEWLINE_ [...]_NEWLINE_]') + .split('_NEWLINE_') + .forEach(line => {line !== '' ? [diff_json_content.push([COLOR.GENERIC(STRING.DIFF.UNCHANGED_SYMBOL_+'│ '+line)])] : null}); //Trailing newline results in a empty line - need to filter it out. + }; + }); + diff_json_content.push(['']) + + // console.log(json_diff_data); + // process.kill(process.pid) + // JSON.stringify((json_diff_data ? json_diff_data : ''),null,2) + // .split('\n') + // .forEach(u => {diff_json_content.push([clc.cyan(u)])}); + + var prev_launches_table = net_tools.readFile(path_prev_launches_table); + const curr_launches_table = format_tools.tabularizeDiffData(await selectData(await data_cache)); + prev_launches_table = prev_launches_table !== '' ? JSON.parse(prev_launches_table) : Array(curr_launches_table.length).fill(Array(curr_launches_table[0].length).fill('')); + diff_table_content = format_tools.diffTable(prev_launches_table,curr_launches_table) + + diff_table_element.setData(diff_table_content ? diff_table_content : [['diff_table_content']]); + diff_json_element.setData(diff_json_content ? diff_json_content : [['diff_json_content']]); + + if (changed) { + const dateStr = new Date().toIsoArr().bw.join(''); + lastDelta = '| '+STRING.LAST_DELTA+COLOR.GENERIC(dateStr); + newData = true; + notifier.notify({ + title: STRING.NEW_DATA, + message: 'Press d to see table diff. Press j to see JSON diff.', + icon: path.join(__dirname, 'spacex.ico'), + appID: STRING.APPID + }); + net_tools.writeFile(path_prev_launches, JSON.stringify(curr_launches)); + net_tools.writeFile(path_prev_launches_table, JSON.stringify(curr_launches_table)); + if (ARCHIVE) { + net_tools.writeFile( + path.join(CONSTANT_VALUES.DATA_PATH,'/archive/'+dateStr.replace(/:/g,'_').replace(' ','_')+'.json'), + JSON.stringify(curr_launches) + ); + net_tools.writeFile( + path.join(CONSTANT_VALUES.DATA_PATH,'/archive/'+dateStr.replace(/:/g,'_').replace(' ','_')+'_table.json'), + JSON.stringify(curr_launches_table) + ); + }; + }; + }; + }; + }; screen.render(); }; console.log('\033[?25l'); //ANSI code - hide cursor -setInterval(main, SCREEN_REFRESH); - - - -function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} \ No newline at end of file +setInterval(main, SCREEN_REFRESH); \ No newline at end of file diff --git a/tools/cli-elements.js b/tools/cli-elements.js index f1b65cf..4456d55 100644 --- a/tools/cli-elements.js +++ b/tools/cli-elements.js @@ -8,7 +8,7 @@ const COLOR = constants.GETCOLORS(clc); const STRING = constants.GETSTRING(COLOR); const STYLES = constants.STYLES; -const control_element_width = STRING.CONTROLS.TABLE.length+8; +const control_element_width = constants.CONTROL_MAX_WIDTH+8; this.screen = blessed.screen({ smartCSR: true, @@ -32,9 +32,9 @@ this.table_element = blessed.listtable({ style: { scrollbar: STYLES.scrollbar }, - scrollbar: true, + scrollbar: !arguments.dump, invertSelected: true, -}) +}); this.status_element = blessed.box({ position : { @@ -102,10 +102,47 @@ this.information_element = blessed.listtable({//blessed.box({ name: 'detailed_information' }); + +this.diff_json_element = blessed.listtable({//blessed.box({ + align: 'left', + position: { + width: '100%', + height: this.screen.height-2, + }, + style: { + scrollbar: STYLES.scrollbar + }, + tags: true, + scrollbar: true, + scrollable: true, + border: 'line', + keys: true, + parent: this.screen, + invertSelected: true, + name: 'diff_json' +}); + +this.diff_table_element = blessed.listtable({ + parent: this.screen, + border: 'line', + align: 'center', + keys: true, + width: "100%", + height: this.screen.height-2, + name: 'diff_table', + tags: true, + style: { + scrollbar: STYLES.scrollbar + }, + scrollbar: true, + invertSelected: true, +}); + this.screen.append(this.status_element); this.screen.append(this.countdown_element); this.screen.append(this.controls_element); this.screen.append(this.information_element); +this.screen.append(this.diff_table_element); this.screen.append(this.table_element); this.table_element.setFront(); this.table_element.focus(); diff --git a/tools/date-tools.js b/tools/date-tools.js index d89c706..e692161 100644 --- a/tools/date-tools.js +++ b/tools/date-tools.js @@ -27,7 +27,7 @@ Date.prototype.toIsoArr = function(precision) { const color = { month: (['half','quarter'].includes(precision) ? COLOR.WARNING : ( ['month','day','hour'].includes(precision) ? COLOR.SUCCESS : COLOR.INVALID )), day: ['day','hour'].includes(precision) ? COLOR.SUCCESS : COLOR.INVALID, - hour: (precision == "hour") ? COLOR.SUCCESS : COLOR.INVALID + hour: (precision === "hour") ? COLOR.SUCCESS : COLOR.INVALID }; col = [ COLOR.SUCCESS(this.getFullYear()), @@ -51,16 +51,16 @@ Date.prototype.toIsoArr = function(precision) { this.secondsHumanReadable = ((t, precision) => { var seconds = parseInt(t, 10); // Convert float to int var days = Math.floor(seconds / 86400); - if (precision == "days") { return days+" days" }; + if (precision === "days") { return days+" days" }; var str = (days != 0 ? days+" days, " : ""); seconds -= days*86400; var hours = Math.floor(seconds / 3600); str += (hours+" hours") - if (precision == "hours") { return str }; + if (precision === "hours") { return str }; seconds -= hours*3600; var minutes = Math.floor(seconds / 60); str += (", "+pad(minutes, " ")+" minutes") - if (precision == "minutes") { return str }; + if (precision === "minutes") { return str }; seconds -= minutes*60; return str+", "+pad(seconds, " ")+" seconds" //Let's not worry about padding the hours.. Doesn't change that much. }); diff --git a/tools/format-tools.js b/tools/format-tools.js index 4d66e80..213055a 100644 --- a/tools/format-tools.js +++ b/tools/format-tools.js @@ -1,17 +1,21 @@ const clc = require('cli-color'); +const constants = require('../constants'); + +const COLOR = constants.GETCOLORS(clc); +const STRING = constants.GETSTRING(COLOR); this.prettyPrintData = ((data_cache, idx, json_view) => { var data = Object.assign({},data_cache[1][idx]); if (json_view) { - var output = [[clc.underline('Key/Value')]]; + var output = [[STRING.HEADERS.JSON]]; JSON.stringify(data,null,2) .split('\n') - .forEach(u => {output.push([clc.cyan(u)])}); - return output + .forEach(u => {output.push([COLOR.GENERIC(u)])}); + return output; } // data.rocket = data_cache[2].filter((rocket) => rocket.id == data.rocket) //I'm lazy right now - was going to add in some features to view the 'hashes' (other elements such as cores, rocket type, etc.), but holding off on it. // console.log(JSON.stringify(data,null,2)) - var output = [[clc.underline('Property'),clc.underline('Value')]]; + var output = [[STRING.HEADERS.KEY,STRING.HEADERS.VALUE]]; JSON.stringify(data,null,2) .split('\n') .forEach(u => { @@ -24,7 +28,141 @@ this.prettyPrintData = ((data_cache, idx, json_view) => { .replace('}','') .replace('{','') .split(/:(.+)/); - output.push([clc.yellow(val[0]),clc.cyan(val[1] ? val[1].replace(/,$/, '') : '')]); + output.push([COLOR.WARNING(val[0]),COLOR.GENERIC(val[1] ? val[1].replace(/,$/, '') : '')]); }); return output; +}); + +this.deepCompare = ((A,B) => { + if (A.length === B.length) { + for ( var i = 0; i < A.length; i++ ) { + if (Array.isArray(A[i])) { + if (Array.isArray(B[i])) { + if (!this.deepCompare(A[i],B[i])) { + return false + }; + } else { + return false; + }; + } else { + if (A[i] !== B[i]) { + return false; + }; + }; + }; + return true; + } else { + return false; + }; +}); + +this.diffTable = ((A,B) => { + var diff_table = [ + [ + '', + STRING.HEADERS.FLIGHT_NUMBER, + STRING.HEADERS.NAME, + STRING.HEADERS.DATE_H, + STRING.HEADERS.PRECISION, + STRING.HEADERS.FLAGS, + STRING.HEADERS.ROCKET, + STRING.HEADERS.CORE, + STRING.HEADERS.LAUNCHPAD, + // STRING.HEADERS.LAUNCHPAD_REG + STRING.HEADERS.PAYLOAD_NAME, + STRING.HEADERS.PAYLOAD_CUSTOMERS, + ], + [ ' ', ' ', ' ', ' ', ' ', ' ', ' ',' ', ' ', ' ', ' '] + ]; + + for ( var i = 0; i < B.length; i++ ) { + var row = [[''],[''],Array(B[i].length).fill('')]; + var mod = false; + for ( var j = 0; j < B[i].length; j++ ) { + var a = A[i][j]; + var b = B[i][j]; + if (a !== b) { + a = COLOR.DANGER(a); + b = COLOR.SUCCESS(b); + mod = true; + } else { + a = COLOR.GENERIC(a); + b = COLOR.GENERIC(b); + }; + row[0].push(a); + row[1].push(b); + }; + if (mod) { + row[0][0] = COLOR.DANGER(STRING.DIFF.PREVIOUS_SYMBOL + STRING.DIFF.PREVIOUS); + row[1][0] = COLOR.SUCCESS(STRING.DIFF.CURRENT_SYMBOL + STRING.DIFF.CURRENT); + } else { + row[0][0] = COLOR.WARNING(STRING.DIFF.UNCHANGED_SYMBOL + STRING.DIFF.PREVIOUS); + row[1][0] = COLOR.WARNING(STRING.DIFF.UNCHANGED_SYMBOL + STRING.DIFF.CURRENT); + }; + diff_table = diff_table.concat(row); + }; + return diff_table; +}); + +this.tabularizeDiffData = (LUNCHES => { + LUNCHES = LUNCHES.map(lunch => [ + String(lunch.flight_number), + lunch.name, + lunch.date_h_, + lunch.date_precision_, + (lunch.tbd ? "tbd" : "")+(lunch.net && lunch.tbd ? ", " : "")+(lunch.net ? "net" : ""), + lunch.rocket, + lunch.cores_, + lunch.launchpad, + // lunch.launchpad_reg + lunch.payloads.names_str, + lunch.payloads.customers_str + ]); + // LUNCHES.unshift([ + // STRING.HEADERS.FLIGHT_NUMBER, + // STRING.HEADERS.NAME, + // STRING.HEADERS.DATE_H, + // STRING.HEADERS.DT, + // STRING.HEADERS.PRECISION, + // STRING.HEADERS.FLAGS, + // STRING.HEADERS.ROCKET, + // STRING.HEADERS.CORE, + // STRING.HEADERS.LAUNCHPAD, + // // STRING.HEADERS.LAUNCHPAD_REG + // STRING.HEADERS.PAYLOAD_NAME, + // STRING.HEADERS.PAYLOAD_CUSTOMERS, + // ],[ ' ', ' ', ' ', ' ', ' ', ' ', ' ',' ', ' ', ' ', ' ']); + return LUNCHES; +}) + +this.tabularizeData = (LUNCHES => { + LUNCHES = LUNCHES.map(lunch => [ + COLOR.GENERIC(String(lunch.flight_number)), + COLOR.GENERIC(lunch.name), + lunch.date_h, + lunch.dt, + lunch.date_precision, + COLOR.DANGER((lunch.tbd ? "tbd" : "")+(lunch.net && lunch.tbd ? ", " : "")+(lunch.net ? "net" : "")), + COLOR.GENERIC(lunch.rocket), + COLOR.GENERIC(lunch.cores), + COLOR.GENERIC(lunch.launchpad), + // lunch.launchpad_reg + COLOR.GENERIC(lunch.payloads.names_str), + COLOR.GENERIC(lunch.payloads.customers_str) + ]); + LUNCHES.unshift([ + STRING.HEADERS.FLIGHT_NUMBER, + STRING.HEADERS.NAME, + STRING.HEADERS.DATE_H, + STRING.HEADERS.DT, + STRING.HEADERS.PRECISION, + STRING.HEADERS.FLAGS, + STRING.HEADERS.ROCKET, + STRING.HEADERS.CORE, + STRING.HEADERS.LAUNCHPAD, + // STRING.HEADERS.LAUNCHPAD_REG + STRING.HEADERS.PAYLOAD_NAME, + STRING.HEADERS.PAYLOAD_CUSTOMERS, + ],[ ' ', ' ', ' ', ' ', ' ', ' ', ' ',' ', ' ', ' ', ' ']); + return LUNCHES; }); \ No newline at end of file diff --git a/tools/net-tools.js b/tools/net-tools.js index bacc851..fd77c1a 100644 --- a/tools/net-tools.js +++ b/tools/net-tools.js @@ -1,4 +1,7 @@ +const fs = require('fs'); +const path = require('path'); const fetch = require('node-fetch'); +const { CONSTANT_VALUES } = require('../constants'); async function fetchURLJSON(url) { var resp; @@ -12,12 +15,42 @@ async function fetchURLJSON(url) { return resp; }; +this.writeFile = ((path_, data) => { + const dir = path_.substring(0, path_.lastIndexOf(path.sep)); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }, (e) => { + if (e) { + throw e; + }; + }); + }; + fs.writeFileSync(path_, data, function(e) { + if (e) { + throw e; + }; + }); +}); + +this.readFile = (path_ => { + if (fs.existsSync(path_)) { + return fs.readFileSync(path_, { encoding: 'utf-8' }, (e,data) => { + if (e) { + throw e; + } else { + return data; + }; + }); + } else { + return ''; //Fail silently - for first-time run where the file doesn't exist. + }; +}); + this.getData = async function() { const LUNCHPADS_RESP = await fetchURLJSON('https://api.spacexdata.com/v4/launchpads'); const LUNCHES_RESP = await fetchURLJSON('https://api.spacexdata.com/v4/launches/upcoming'); const ROCKETS_RESP = await fetchURLJSON('https://api.spacexdata.com/v4/rockets'); const PAYLOADS_RESP = await fetchURLJSON('https://api.spacexdata.com/v4/payloads'); - const CORES_RESP = await fetchURLJSON('https://api.spacexdata.com/v4/cores'); + const CORES_RESP = await fetchURLJSON('https://api.spacexdata.com/v4/cores'); return [LUNCHPADS_RESP, LUNCHES_RESP, ROCKETS_RESP, PAYLOADS_RESP, CORES_RESP]; }; diff --git a/tools/process-args.js b/tools/process-args.js index 3b04cb6..820dd6d 100644 --- a/tools/process-args.js +++ b/tools/process-args.js @@ -9,19 +9,34 @@ this.arguments = mri(process.argv, { help: false, screen_refresh: String(1000), color: true, - dump: false + dump: false, + blink: false, + archive: false, + path: '~/.spacexcli', + notify_time: String(5400), + highlight_time: String(86400), }, alias: { api_refresh: "a", help: "h", screen_refresh: "s", color: "c", - dump: "d" + dump: "d", + blink: "b", + archive: "v", + path: "p", + notify_time: "n", + highlight_time: "g", } }); -['s', 'screen_refresh', 'a', 'api_refresh'].forEach(idx => { - if (idx == 'a' || idx == 'api_refresh') { +[ + 's', 'screen_refresh', + 'a', 'api_refresh', + 'n', 'notify_time', + 'g', 'highlight_time', +].forEach(idx => { + if (idx === 'a' || idx === 'api_refresh') { if (this.arguments[idx] < 30*1000) { this.arguments[idx] = 30*1000 // No spam pls! } @@ -33,8 +48,8 @@ if (this.arguments.help) { console.log(` Usage: spacex-cli - spacex-cli [-a ] | [-h] | [-s ] | [-d] - spacex-cli [--api_refresh=] | [--help] | [--screen_refresh=] | [--dump] + spacex-cli [-a ] | [-h] | [-s ] | [-d] | [-b] + spacex-cli [--api_refresh=] | [--help] | [--screen_refresh=] | [--dump] | [--blink] Options: -h, --help Show this help information. @@ -42,6 +57,11 @@ Options: -a, --api_refresh API refresh interval in milliseconds. How often we poll the api for new/updated information. Please don't use small values! [default: 600000] -c, --color Print with color [default: true] -d, --dump Non-interactive mode - dumps the main launches table [default: false] + -b, --blink Blink for close launches. This argument exists because I know some people hate blink [default: false] + -v, --archive Archive launch data when changed [default: false] + -p, --path Application directory [default: ~/.spacexcli] + -n, --notify_time At this amount of seconds remaining until launch, send a notification [default: 5400] + -g, --highlight_time At this amount of seconds remaining until launch, highlight the row in the table view [default: 86400] Current configuration:`); var arguments_ = this.arguments;