Files
bare-operating-system/packages/bare-os-booter/lib/boot-splash.js
T
Raven Scott 342b6040fe
CI / test (push) Successful in 9m48s
Update
2026-04-03 02:49:18 -04:00

188 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* TTY boot splash: timer, spinner, status lines, full-screen redraw.
* Disable with BARE_OS_NO_SPLASH=1 or when stdout is not a TTY (falls back to no-op).
*/
const CLEAR = '\x1b[3J\x1b[2J\x1b[H'
const HIDE_CURSOR = '\x1b[?25l'
const SHOW_CURSOR = '\x1b[?25h'
const RESET = '\x1b[0m'
const DIM = '\x1b[2m'
const BOLD = '\x1b[1m'
const GREEN = '\x1b[32m'
const MAGENTA = '\x1b[35m'
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
/** Seconds for one left→right shimmer pass (iPhone-style slide-to-unlock pacing). */
const TITLE_SHIMMER_PERIOD_SEC = 7.5
/** How wide the bright band is in character positions (Gaussian sigma). */
const TITLE_SHIMMER_SIGMA = 2.8
const TITLE_BASE_RGB = [58, 62, 72]
const TITLE_PEAK_RGB = [238, 240, 245]
/**
* Truecolor title line with a slow moving highlight (no fast hue stepping).
* @param {number} tSec
*/
function formatTitleShimmer(tSec) {
const label = ' ◆ BARE-OS ◆'
const n = label.length
const u = (tSec / TITLE_SHIMMER_PERIOD_SEC) % 1
const center = u * (n + 2) - 1
let out = ''
const [br, bg, bb] = TITLE_BASE_RGB
const [pr, pg, pb] = TITLE_PEAK_RGB
for (let i = 0; i < n; i++) {
const dist = i - center
const w = Math.exp(-(dist * dist) / (2 * TITLE_SHIMMER_SIGMA * TITLE_SHIMMER_SIGMA))
const r = Math.round(br + (pr - br) * w)
const g = Math.round(bg + (pg - bg) * w)
const b = Math.round(bb + (pb - bb) * w)
const ch = label[i]
out += `\x1b[38;2;${r};${g};${b}m${BOLD}${ch}`
}
return out + RESET
}
/** @returns {boolean} */
function splashDisabled() {
return globalThis.process?.env?.BARE_OS_NO_SPLASH === '1'
}
/**
* @param {import('stream').Writable | null | undefined} stdout
* @param {{ bootLimitMs?: number }} [opts]
*/
export function createBootSplash(stdout, opts = {}) {
const bootLimitMs = opts.bootLimitMs ?? 60_000
if (
splashDisabled() ||
!stdout ||
typeof stdout.write !== 'function' ||
!stdout.isTTY
) {
return {
start() {},
stop() {},
setPhase() {},
log() {},
prepareForKernel() {},
fail() {}
}
}
let t0 = 0
let frame = 0
/** @type {ReturnType<typeof setInterval> | null} */
let tick = null
let phase = 'Starting…'
/** @type {string[]} */
const lines = []
const maxLines = 8
function elapsedSec() {
return (Date.now() - t0) / 1000
}
function draw() {
const w = Math.max(40, Number(stdout.columns) || 72)
const el = elapsedSec()
const sp = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]
frame++
const barW = Math.min(36, w - 8)
const progress = Math.min(1, el / (bootLimitMs / 1000))
const filled = Math.round(barW * progress)
const bar =
GREEN + '█'.repeat(filled) + DIM + '░'.repeat(Math.max(0, barW - filled)) + RESET
const title = formatTitleShimmer(el)
const sub = `${DIM} network boot · ${(bootLimitMs / 1000).toFixed(0)}s limit${RESET}`
const logBlock = lines.length
? lines.map((l) => ` ${DIM}${RESET} ${l}`).join('\n') + '\n'
: ''
const timeLine = ` ${BOLD}Boot${RESET} ${el.toFixed(1)}s ${sp} ${phase}`
const art = [
'',
title,
sub,
'',
` ${bar}`,
'',
logBlock,
timeLine,
''
].join('\n')
const out = CLEAR + HIDE_CURSOR + art
try {
stdout.write(out)
} catch {
/* ignore */
}
}
return {
start() {
t0 = Date.now()
frame = 0
phase = 'Initializing…'
lines.length = 0
if (tick) clearInterval(tick)
draw()
tick = setInterval(draw, 90)
},
stop() {
if (tick) {
clearInterval(tick)
tick = null
}
},
/** @param {string} msg */
setPhase(msg) {
phase = msg
},
/** @param {string} msg */
log(msg) {
lines.push(msg)
while (lines.length > maxLines) lines.shift()
},
/** Second clear before interactive shell / fish. */
prepareForKernel() {
this.stop()
try {
stdout.write(CLEAR + SHOW_CURSOR + RESET)
} catch {
/* ignore */
}
},
/** Show error then restore cursor. */
fail(msg) {
this.stop()
try {
stdout.write(
CLEAR +
SHOW_CURSOR +
MAGENTA +
msg +
RESET +
'\n'
)
} catch {
/* ignore */
}
}
}
}