#!/usr/bin/env /opt/homebrew/bin/zx const HOSTS = [ 'cdn.search.brave.com', 'cloudflare.com', 'github.com', 'dns.google', 'time.google.com', 'logseq.com', 'de.pool.ntp.org', 'steampowered.com', 'mandelbaum.ai', ] const PING_COUNT = 10 const PING_INTERVAL = 0.2 const PING_MAX = 1000 const PING_CONSIDERED_HIGH = 150 const DISPLAY_MIN = 5 const HIST_MIN = 180 const HIST_PATH = '/tmp/ping_bars.json' const FONT_MONO = 'font=Monaco alternate=false' const COLOR_THRESHOLDS = [ [10, '#00FFFF'], [20, '#00E6BF'], [30, '#00CC80'], [40, '#00B340'], [50, '#009900'], [60, '#99D600'], [70, '#CCEB00'], [80, '#F1E033'], [90, '#FF9900'], [100, '#D84D00'], [125, '#B00000'], [150, '#CB0000'], [175, '#E50000'], [200, '#FF0000'], [250, '#D20631'], [300, '#A60D62'], [350, '#791393'], ] const LEVEL_OFFLINE = ['?', '#ff00f0', PING_MAX] const LEVEL_SET = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'] const INTERVAL = parseInt(path.basename(__filename).match(/\.(\d+).*\./)[1]) const DISPLAY_LEN = (DISPLAY_MIN * 60) / INTERVAL const HIST_LEN = (HIST_MIN * 60) / INTERVAL const ABORT_TIMEOUT = INTERVAL * 0.9 /** * @param {string} res * @returns {[avg: number, jitter: number]} */ const parsePing = res => { if (res.exitCode !== 0) return [PING_MAX, 0] const match = res.stdout.match(/min\/avg\/max\/stddev = [\d.]+\/([\d.]+)\/[\d.]+\/([\d.]+)/) if (!match) return [PING_MAX, 0] // TODO: add jitter match return Math.round(parseFloat(match[1])) } const linearGrowth = (start, end, count) => Array.from({ length: count }, (_, i) => Math.round(start + ((end - start) * i) / (count - 1))) const getLevel = (time, max) => { const set = LEVEL_SET const symbolThresholds = linearGrowth(1, max, set.length) const symbolIndex = symbolThresholds.findIndex(threshold => time < threshold) if (symbolIndex === -1) return LEVEL_OFFLINE const colorIndex = COLOR_THRESHOLDS.findIndex(([threshold]) => time <= threshold) if (colorIndex === -1) return LEVEL_OFFLINE return [set[symbolIndex], COLOR_THRESHOLDS[colorIndex][1], symbolThresholds[symbolIndex]] } const getHistory = async () => { try { return JSON.parse(await fs.readFile(HIST_PATH, 'utf8')).slice(-(HIST_LEN - 1)) } catch { return [] } } const saveHistory = h => fs.writeFile(HIST_PATH, JSON.stringify(h.length > HIST_LEN ? h.slice(-HIST_LEN) : h)) const getGraph = (h, high) => { const bars = h .map(v => getLevel(v, high)) .map(v => v[0]) .join('') if (h.length >= DISPLAY_MIN && bars.length !== h.length) return `??? "${bars}" (b ${bars.length}, h ${h})` return bars } const getAvg = arr => Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) const getStats = (arr = []) => { const valid = arr.filter(t => t < PING_MAX) return { min: Math.round(Math.min(...valid)), avg: getAvg(valid), max: Math.round(Math.max(...valid)), timeout: arr.length - valid.length, } } const table = (data, params = '', header = true, fill = ' ', sep = ' ') => { const allParams = ` | trim=false ${FONT_MONO} ${params}` const widths = data[0].map((_, i) => Math.max(...data.map(r => String(r[i]).length))) return data .map(row => row.map((col, i) => String(col).padStart(widths[i], fill)).join(sep)) .join('\n') .replace(/$/gm, allParams) } try { const pingStart = Date.now() const PING_COMMANDS = HOSTS.map( site => `ping -i ${PING_INTERVAL} -c ${PING_COUNT} -t ${ABORT_TIMEOUT} -n -q '${site}'` ) // TODO: USE PING_COMMANDS const PING_PROMISES = HOSTS.map(site => $`ping -i ${PING_INTERVAL} -c ${PING_COUNT} -t ${ABORT_TIMEOUT} -n -q ${site}`.catch(e => e) ) const responses = await Promise.all(PING_PROMISES) const pingEnd = Date.now() const times = responses.map(parsePing) const stats = getStats(times) const data = await getHistory() data.push({ avg: stats.avg, time: Date.now() }) const hist = data.map(v => v.avg) saveHistory(data) const histStat = getStats(hist) const tri = hist.slice(-(DISPLAY_LEN * 4)) const triStat = getStats(tri) const display = hist.slice(-DISPLAY_LEN) const displayStat = getStats(display) const graph = getGraph(display, PING_CONSIDERED_HIGH) const [_, color] = getLevel(stats.avg, PING_MAX) console.log(`${graph} ⚡️${stats.avg}ms | color=${color} ${FONT_MONO}`) console.log('---') console.log( table([ [``, `L`, `M`, `H`, `?`], [`${INTERVAL}s`, stats.min, stats.avg, stats.max, `${stats.timeout}x`], [`${DISPLAY_MIN}m`, displayStat.min, displayStat.avg, displayStat.max, `${displayStat.timeout}x`], [`${Math.round((tri.length / 60) * INTERVAL)}m`, triStat.min, triStat.avg, triStat.max, `${triStat.timeout}x`], [`${HIST_MIN}m`, histStat.min, histStat.avg, histStat.max, `${histStat.timeout}x`], ]) ) console.log('---') HOSTS.forEach((site, i) => { // TODO: Show jitter console.log(`${site}: ${times[i]}ms`) console.log(`${PING_COMMANDS[i]} | alternate=true bash=/bin/sh param0="-c" param1='${PING_COMMANDS[i]}'`) }) console.log('---') const runtime = (pingEnd - pingStart) / 1000 const uptime = process.uptime() - runtime console.log(`Runtime: ${runtime.toFixed(3)}s / ${uptime.toFixed(3)}s`) console.log(`Refresh... | refresh=true`) } catch (error) { console.log(`Error (check details)`) console.log(error) }