source via build.mjs). Ship lib/baresay-engine.js for wrapping, balloons, and cowfile parsing; default character is bear under /share/baresay/.cow. Register commands, man pages, rebuild man/completion/posix index, sync kernel to seeder, bump Tier-1 command count in docs to 155, and add a test that say and baresay bundles match.
503 lines
13 KiB
Plaintext
503 lines
13 KiB
Plaintext
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
|
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
|
|
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
|
function bareFormatModeString(mode, type) {
|
|
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
|
const perm = mode & 0o777
|
|
const r = (bit) => (perm & bit ? 'r' : '-')
|
|
const w = (bit) => (perm & bit ? 'w' : '-')
|
|
const x = (bit) => (perm & bit ? 'x' : '-')
|
|
return (
|
|
typeChar +
|
|
r(0o400) +
|
|
w(0o200) +
|
|
x(0o100) +
|
|
r(0o040) +
|
|
w(0o020) +
|
|
x(0o010) +
|
|
r(0o004) +
|
|
w(0o002) +
|
|
x(0o001)
|
|
)
|
|
}
|
|
|
|
/** @param {number} mtimeMs @param {number} [nowMs] */
|
|
function bareFormatLsMtime(mtimeMs, nowMs) {
|
|
const now = nowMs != null ? nowMs : Date.now()
|
|
const d = new Date(mtimeMs)
|
|
const months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec'
|
|
]
|
|
const mon = months[d.getMonth()]
|
|
const day = String(d.getDate()).padStart(2, ' ')
|
|
const sixMo = 180 * 24 * 3600 * 1000
|
|
if (Math.abs(now - mtimeMs) > sixMo) {
|
|
const yr = String(d.getFullYear()).padStart(4, ' ')
|
|
return mon + ' ' + day + ' ' + yr
|
|
}
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
return mon + ' ' + day + ' ' + hh + ':' + mm
|
|
}
|
|
|
|
/** @param {number} size */
|
|
function barePosixBlocks(size) {
|
|
return Math.ceil(Number(size) / 512) || 0
|
|
}
|
|
|
|
/**
|
|
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
|
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string | Uint8Array} chunk
|
|
* @returns {boolean}
|
|
*/
|
|
function bareOsEmitRaw(ctx, chunk) {
|
|
if (typeof ctx.bareOsBinWrite === 'function') {
|
|
const b4 = ctx.b4a
|
|
const u8 =
|
|
typeof chunk === 'string'
|
|
? b4 && typeof b4.from === 'function'
|
|
? b4.from(chunk)
|
|
: new TextEncoder().encode(chunk)
|
|
: chunk
|
|
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
|
return true
|
|
}
|
|
const w = globalThis.process?.stdout?.write
|
|
if (typeof w === 'function') {
|
|
w.call(globalThis.process.stdout, chunk)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Bubble + cow rendering for baresay (no imports; prepended before src/baresay.js).
|
|
*/
|
|
|
|
/** @param {string} text @param {number} width @param {boolean} noWrap */
|
|
function bareBaresayWrap(text, width, noWrap) {
|
|
const t = String(text || '').replace(/\r\n/g, '\n')
|
|
if (noWrap) {
|
|
const line = t.replace(/\n/g, ' ')
|
|
return line.length ? [line] : ['']
|
|
}
|
|
const out = []
|
|
const lim = Math.max(8, Math.min(200, width | 0)) || 40
|
|
for (const para of t.split('\n')) {
|
|
if (!para.trim()) {
|
|
out.push('')
|
|
continue
|
|
}
|
|
let rest = para.trim()
|
|
while (rest.length) {
|
|
if (rest.length <= lim) {
|
|
out.push(rest)
|
|
break
|
|
}
|
|
let cut = rest.lastIndexOf(' ', lim)
|
|
if (cut <= 0) cut = lim
|
|
const piece = rest.slice(0, cut).trimEnd()
|
|
if (!piece.length) {
|
|
out.push(rest.slice(0, lim))
|
|
rest = rest.slice(lim).trimStart()
|
|
} else {
|
|
out.push(piece)
|
|
rest = rest.slice(cut).trimStart()
|
|
}
|
|
}
|
|
}
|
|
return out.length ? out : ['']
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} lines
|
|
* @param {{ think?: boolean, rainbow?: boolean }} opts
|
|
*/
|
|
function bareBaresayBalloon(lines, opts) {
|
|
const think = !!(opts && opts.think)
|
|
const rainbow = !!(opts && opts.rainbow)
|
|
const rows = lines.map((l) => String(l))
|
|
const n = rows.length
|
|
const longest = Math.max(...rows.map((r) => r.length), 2)
|
|
const padded = rows.map((r) => r.padEnd(longest))
|
|
|
|
/** @type {string[]} */
|
|
const mid = []
|
|
if (n === 1) {
|
|
const inner = think
|
|
? '( ' + padded[0] + ' )'
|
|
: '< ' + padded[0] + ' >'
|
|
mid.push(inner)
|
|
} else if (think) {
|
|
for (let i = 0; i < n; i++) {
|
|
mid.push('( ' + padded[i] + ' )')
|
|
}
|
|
} else {
|
|
for (let i = 0; i < n; i++) {
|
|
let L = '|'
|
|
let R = '|'
|
|
if (i === 0) {
|
|
L = '/'
|
|
R = '\\'
|
|
} else if (i === n - 1) {
|
|
L = '\\'
|
|
R = '/'
|
|
}
|
|
mid.push(L + ' ' + padded[i] + ' ' + R)
|
|
}
|
|
}
|
|
|
|
const topUnders = Math.max(0, mid[0].length - 2)
|
|
const botDash = Math.max(0, mid[mid.length - 1].length - 2)
|
|
/** @type {string[]} */
|
|
const out = []
|
|
out.push(' ' + '_'.repeat(topUnders))
|
|
if (rainbow) {
|
|
const ansi = bareBaresayRainbowCodes()
|
|
let k = 0
|
|
for (const line of mid) {
|
|
const c = ansi[k % ansi.length]
|
|
k++
|
|
out.push(c + line + '\x1b[0m')
|
|
}
|
|
} else {
|
|
for (const line of mid) out.push(line)
|
|
}
|
|
out.push(' ' + '-'.repeat(botDash))
|
|
return out
|
|
}
|
|
|
|
function bareBaresayRainbowCodes() {
|
|
return [
|
|
'\x1b[31m',
|
|
'\x1b[91m',
|
|
'\x1b[33m',
|
|
'\x1b[32m',
|
|
'\x1b[36m',
|
|
'\x1b[34m',
|
|
'\x1b[35m'
|
|
]
|
|
}
|
|
|
|
/** Extract cow body from .cow text (Perl heredoc or raw template). */
|
|
function bareBaresayExtractCow(raw) {
|
|
const s = String(raw || '').replace(/^\ufeff/, '')
|
|
const m = s.match(/\$the_cow\s*=\s*<<"EOC"\s*\r?\n([\s\S]*?)\r?\nEOC\b/)
|
|
if (m) return m[1].replace(/\r\n/g, '\n')
|
|
return s.replace(/\r\n/g, '\n').trimEnd() + '\n'
|
|
}
|
|
|
|
/**
|
|
* @param {string} template
|
|
* @param {string} eyes two chars
|
|
* @param {string} tongue one char
|
|
* @param {string} thoughts
|
|
*/
|
|
function bareBaresayFillCow(template, eyes, tongue, thoughts) {
|
|
const e = (eyes || 'oo').slice(0, 2).padEnd(2, 'o')
|
|
const tg = (tongue || ' ').slice(0, 1)
|
|
const th = thoughts || '\\'
|
|
return template
|
|
.replace(/\$thoughts/g, th)
|
|
.replace(/\$eyes/g, e)
|
|
.replace(/\$tongue/g, tg)
|
|
}
|
|
|
|
/** @param {string} flag */
|
|
function bareBaresayPreset(flag) {
|
|
switch (flag) {
|
|
case 'b':
|
|
return { eyes: '==', tongue: ' ' }
|
|
case 'd':
|
|
return { eyes: 'XX', tongue: 'U' }
|
|
case 'g':
|
|
return { eyes: '$$', tongue: ' ' }
|
|
case 'p':
|
|
return { eyes: '@@', tongue: ' ' }
|
|
case 's':
|
|
return { eyes: '**', tongue: ' ' }
|
|
case 't':
|
|
return { eyes: '--', tongue: ' ' }
|
|
case 'w':
|
|
return { eyes: '@@', tongue: ' ' }
|
|
case 'y':
|
|
return { eyes: '..', tongue: ' ' }
|
|
default:
|
|
return { eyes: 'oo', tongue: ' ' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} message
|
|
* @param {string} cowBody filled lines (no vars)
|
|
* @param {{ width: number, noWrap: boolean, think?: boolean, rainbow?: boolean }} opts
|
|
*/
|
|
function bareBaresayCompose(message, cowBody, opts) {
|
|
const width = opts.width | 0
|
|
const wrapped = bareBaresayWrap(message, width, opts.noWrap)
|
|
const balloon = bareBaresayBalloon(wrapped, {
|
|
think: opts.think,
|
|
rainbow: opts.rainbow
|
|
})
|
|
const cowLines = cowBody.replace(/\s+$/, '').split('\n')
|
|
return balloon.concat(cowLines).join('\n') + '\n'
|
|
}
|
|
|
|
/** Minimal bear if no cowfile on disk (ASCII). */
|
|
function bareBaresayFallbackBearTemplate() {
|
|
return [
|
|
' $thoughts',
|
|
' $thoughts .--.--.',
|
|
' $thoughts ( @ @ )',
|
|
' $thoughts / $eyes \\',
|
|
' $thoughts | $tongue |',
|
|
' $thoughts \\______/',
|
|
' $thoughts ||',
|
|
' $thoughts ~~'
|
|
].join('\n')
|
|
}
|
|
|
|
async function run(ctx, argv) {
|
|
const raw0 = argv && argv.length ? argv[0] : 'baresay'
|
|
const slash = String(raw0).lastIndexOf('/')
|
|
const prog =
|
|
slash >= 0 ? String(raw0).slice(slash + 1) || 'baresay' : String(raw0)
|
|
|
|
/** @param {string} msg */
|
|
const usage = (msg) => {
|
|
if (msg) ctx.console.error(prog + ': ' + msg)
|
|
ctx.console.error(
|
|
'usage: ' +
|
|
prog +
|
|
' [-h] [-l] [-n] [-W COLS] [-f name|path] [-e XX] [-T C] [-bdgpstwy] [--think] [--rainbow] [-R|--random] [MESSAGE...]\n' +
|
|
' ' +
|
|
prog +
|
|
' reads stdin when no MESSAGE and stdin is non-empty.'
|
|
)
|
|
ctx.exitCode = 2
|
|
}
|
|
|
|
let width = 40
|
|
let noWrap = false
|
|
let cowKey = 'bear'
|
|
let cowPath = ''
|
|
let listOnly = false
|
|
let eyes = 'oo'
|
|
let tongue = ' '
|
|
let think = false
|
|
let rainbow = false
|
|
let randomPick = false
|
|
/** @type {string[]} */
|
|
const operands = []
|
|
|
|
for (let i = 1; i < argv.length; i++) {
|
|
const a = argv[i]
|
|
if (a === '--') {
|
|
operands.push(...argv.slice(i + 1))
|
|
break
|
|
}
|
|
if (a === '-h' || a === '--help') {
|
|
usage('')
|
|
return
|
|
}
|
|
if (a === '-l') {
|
|
listOnly = true
|
|
continue
|
|
}
|
|
if (a === '-n') {
|
|
noWrap = true
|
|
continue
|
|
}
|
|
if (a === '--think' || a === '-th') {
|
|
think = true
|
|
continue
|
|
}
|
|
if (a === '--rainbow') {
|
|
rainbow = true
|
|
continue
|
|
}
|
|
if (a === '-R' || a === '--random') {
|
|
randomPick = true
|
|
continue
|
|
}
|
|
if (a === '-W' || a === '--width') {
|
|
const v = argv[++i]
|
|
const n = v != null ? parseInt(String(v), 10) : NaN
|
|
if (!Number.isFinite(n) || n < 1) {
|
|
usage('invalid width')
|
|
return
|
|
}
|
|
width = n
|
|
continue
|
|
}
|
|
if (a.startsWith('-W') && a.length > 2) {
|
|
const n = parseInt(a.slice(2), 10)
|
|
if (!Number.isFinite(n) || n < 1) {
|
|
usage('invalid width')
|
|
return
|
|
}
|
|
width = n
|
|
continue
|
|
}
|
|
if (a === '-f' || a === '--file') {
|
|
const v = argv[++i]
|
|
if (v == null) {
|
|
usage('missing cow file')
|
|
return
|
|
}
|
|
if (v.startsWith('/') || v.startsWith('.')) cowPath = v
|
|
else cowKey = v.replace(/\.cow$/i, '')
|
|
continue
|
|
}
|
|
if (a.startsWith('-f') && a.length > 2) {
|
|
const v = a.slice(2)
|
|
if (v.startsWith('/') || v.startsWith('.')) cowPath = v
|
|
else cowKey = v.replace(/\.cow$/i, '')
|
|
continue
|
|
}
|
|
if (a === '-e') {
|
|
const v = argv[++i]
|
|
if (v == null || String(v).length < 1) {
|
|
usage('missing eyes')
|
|
return
|
|
}
|
|
eyes = String(v).slice(0, 2).padEnd(2, ' ')
|
|
continue
|
|
}
|
|
if (a.startsWith('-e') && a.length > 2) {
|
|
eyes = a.slice(2, 4).padEnd(2, ' ')
|
|
continue
|
|
}
|
|
if (a === '-T') {
|
|
const v = argv[++i]
|
|
tongue = v != null && String(v).length ? String(v).slice(0, 1) : ' '
|
|
continue
|
|
}
|
|
if (a.startsWith('-T') && a.length > 2) {
|
|
tongue = a.slice(2, 3) || ' '
|
|
continue
|
|
}
|
|
{
|
|
const mm = a.match(/^-([bdgpstwy])$/)
|
|
if (mm) {
|
|
const p = bareBaresayPreset(mm[1])
|
|
eyes = p.eyes
|
|
tongue = p.tongue
|
|
continue
|
|
}
|
|
}
|
|
if (a.startsWith('-') && a.length > 1) {
|
|
usage('unknown option: ' + a)
|
|
return
|
|
}
|
|
operands.push(a)
|
|
}
|
|
|
|
const vfs = ctx.vfs
|
|
const b4 = ctx.b4a
|
|
|
|
/** @returns {Promise<string[]>} */
|
|
async function listShareCows() {
|
|
const dir = '/share/baresay'
|
|
/** @type {string[]} */
|
|
const names = []
|
|
if (!vfs || typeof vfs.readdir !== 'function') return names
|
|
try {
|
|
for await (const n of vfs.readdir(dir)) {
|
|
if (typeof n === 'string' && n.endsWith('.cow')) {
|
|
names.push(n.replace(/\.cow$/i, ''))
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return names.sort()
|
|
}
|
|
|
|
if (listOnly) {
|
|
const names = await listShareCows()
|
|
for (const n of names) ctx.console.log(n)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
|
|
/** @type {string} */
|
|
let message = operands.join(' ')
|
|
if (!message.trim()) {
|
|
message = bareStdin(ctx).replace(/\r\n/g, '\n').trimEnd()
|
|
}
|
|
if (randomPick) {
|
|
const names = await listShareCows()
|
|
const pool = names.length ? names : ['bear']
|
|
cowKey = pool[Math.floor(Math.random() * pool.length)]
|
|
cowPath = ''
|
|
}
|
|
|
|
/** @returns {Promise<string | null>} */
|
|
async function readCowAt(path) {
|
|
if (!vfs || typeof vfs.readFile !== 'function') return null
|
|
try {
|
|
const buf = await vfs.readFile(path)
|
|
if (!buf) return null
|
|
return b4 ? b4.toString(buf) : String(buf)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
let rawCow = ''
|
|
if (cowPath) {
|
|
rawCow = (await readCowAt(cowPath)) || ''
|
|
} else {
|
|
const p = '/share/baresay/' + cowKey + '.cow'
|
|
rawCow = (await readCowAt(p)) || ''
|
|
}
|
|
if (!rawCow.trim()) {
|
|
if (cowKey === 'bear' && !cowPath) {
|
|
rawCow = bareBaresayFallbackBearTemplate()
|
|
} else {
|
|
ctx.console.error(
|
|
prog +
|
|
': cow file not found: ' +
|
|
(cowPath || '/share/baresay/' + cowKey + '.cow')
|
|
)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
}
|
|
|
|
const template = bareBaresayExtractCow(rawCow)
|
|
const filled = bareBaresayFillCow(template, eyes, tongue, '\\')
|
|
const out = bareBaresayCompose(message, filled, {
|
|
width,
|
|
noWrap,
|
|
think,
|
|
rainbow
|
|
})
|
|
|
|
const w = globalThis.process?.stdout?.write
|
|
if (typeof w === 'function') {
|
|
w.call(globalThis.process.stdout, out)
|
|
} else {
|
|
ctx.console.log(out.replace(/\n$/, ''))
|
|
}
|
|
ctx.exitCode = 0
|
|
}
|