Add cowsay-style /bin/baresay and identical /bin/say (say maps to baresay
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.
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
|
||||
## Contents
|
||||
|
||||
- `**init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: `**/etc/os-release**` → `**/etc/motd**` → optional `**/etc/bare-os/rc.profile.<profile>**` (profile from `**BARE_OS_BOOT_PROFILE**` or first line of `**/etc/bare-os/profile**`; the booter mirrors the resolved name in `**ctx.env.BARE_OS_BOOT_PROFILE_RESOLVED**` and `**/run/bare-os/boot_profile**`) → `**/etc/bare-os/rc**` → `**/etc/bare-os/rc.d/***` (sorted; digit-prefixed names only; skip dotfiles, `*~`, `README*`, `*.md`; optional `**BARE_OS_RC_D_SKIP**` comma list and `**prefix***` patterns) → optional `**/etc/bare-os/rc.local**` → `**/etc/bare-os/kernel.d/***` (same rules as `**rc.d**`) → banner → when `**BARE_OS_SKIP_REPL**`, optional **onboot** lines from `**BARE_OS_ONBOOT`** or `**/etc/bare-os/onboot**` → `**readLine` / `execLine**` loop. Boot `**execLine**` errors in trusted snippets are logged; with `**BARE_OS_BOOT_STRICT=1**` or `**true**`, the first throw calls `**requestBooterExit(1)**` and stops later boot phases. Custom kernels may call `**ctx.registerKernelShutdownHook(fn)**` before initd disposers; use `**ctx.bareOsRuntimeCaps**` for limits, pseudo paths, and `**features**` (`[developer-guide/02-the-context-object.md](../developer-guide/02-the-context-object.md)`).
|
||||
- `**bin/**` — **Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md) plus `**sshd`** / `**bare-sshd**` from [bare-os-openssh](../packages/bare-os-openssh/) (**153** commands in `**COREUTILS_COMMANDS`**; `**sshd**` is listed for man/help but its concatenated script is emitted by the openssh package build, not coreutils `**src/**`). Each file is `**runtime.js**` + optional preamble (`**lib/md5.js**` for `**md5sum**`, `**lib/sha224.js**` for `**sha224sum**`, `**lib/*-engine.js**` for `**sed**`/`**awk**`, `**jq-engine.js**`, `**lib/man-render.js**`, `**lib/edit-*.js**` for `**edit**`/`**nano**`, lscolors for `**ls**`/`**dircolors**`, …) + `**async function run(ctx, argv)**` (no ESM `**import**` in `**src/**`). `**/bin/nano**` duplicates `**/bin/edit**` for familiarity; the shell’s default `**nano` → `edit**` alias uses the `**edit**` command name after expansion. `**dir**`/`**vdir**` invoke `**ls**` via `**ctx.runBinCommand**`.
|
||||
- `**bin/**` — **Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md) plus `**sshd`** / `**bare-sshd**` from [bare-os-openssh](../packages/bare-os-openssh/) (**155** commands in `**COREUTILS_COMMANDS`**; `**sshd**` is listed for man/help but its concatenated script is emitted by the openssh package build, not coreutils `**src/**`). Each file is `**runtime.js**` + optional preamble (`**lib/md5.js**` for `**md5sum**`, `**lib/sha224.js**` for `**sha224sum**`, `**lib/*-engine.js**` for `**sed**`/`**awk**`, `**jq-engine.js**`, `**lib/man-render.js**`, `**lib/edit-*.js**` for `**edit**`/`**nano**`, lscolors for `**ls**`/`**dircolors**`, …) + `**async function run(ctx, argv)**` (no ESM `**import**` in `**src/**`). `**/bin/nano**` duplicates `**/bin/edit**` for familiarity; the shell’s default `**nano` → `edit**` alias uses the `**edit**` command name after expansion. `**dir**`/`**vdir**` invoke `**ls**` via `**ctx.runBinCommand**`.
|
||||
- `**lib/bare/**` — Optional IIFE bundles + `**manifest.json**` for `**ctx.bare**` drive merge, built by [bare-os-bare-libs](../packages/bare-os-bare-libs/README.md). Same trust model as `**bin/**` (trusted seeded image).
|
||||
- `**share/man/man.json**` — Merged manual database for `**/bin/man**` (built by `**bare-os-coreutils**`; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
|
||||
- `**etc/os-release**` — Static OS metadata (`NAME`, `VERSION`, …).
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
/* 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
|
||||
}
|
||||
+1
-1
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
var BARE_OS_HELP_BIN_SPACED = "arch awk baretop base32 base64 basename basenc btop bundlebee cat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs xattr yes"
|
||||
var BARE_OS_HELP_BIN_SPACED = "arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs xattr yes"
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(
|
||||
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
||||
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
/* 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
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-21T23:08:29.149Z",
|
||||
"generatedAt": "2026-04-21T23:27:43.821Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
@@ -12,6 +12,10 @@
|
||||
"name": "awk",
|
||||
"tier": "tier1_bin"
|
||||
},
|
||||
{
|
||||
"name": "baresay",
|
||||
"tier": "tier1_bin"
|
||||
},
|
||||
{
|
||||
"name": "baretop",
|
||||
"tier": "tier1_bin"
|
||||
@@ -420,6 +424,10 @@
|
||||
"name": "savevault",
|
||||
"tier": "tier1_bin"
|
||||
},
|
||||
{
|
||||
"name": "say",
|
||||
"tier": "tier1_bin"
|
||||
},
|
||||
{
|
||||
"name": "sed",
|
||||
"tier": "tier1_bin"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1776812909149,
|
||||
"atMs": 1776814063820,
|
||||
"commands": [
|
||||
"arch",
|
||||
"awk",
|
||||
"baresay",
|
||||
"baretop",
|
||||
"base32",
|
||||
"base64",
|
||||
@@ -106,6 +107,7 @@
|
||||
"rm",
|
||||
"rmdir",
|
||||
"savevault",
|
||||
"say",
|
||||
"sed",
|
||||
"seq",
|
||||
"setfacl",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts .--. Bare OS
|
||||
$thoughts | ( $eyes ) |
|
||||
$thoughts | ( $tongue ) |
|
||||
$thoughts | --- |
|
||||
$thoughts `-- mm --'
|
||||
$thoughts ||
|
||||
EOC
|
||||
@@ -0,0 +1,8 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts /|
|
||||
$thoughts _/ |__
|
||||
$thoughts > $eyes $tongue <
|
||||
$thoughts ^------^
|
||||
$thoughts ~~ ~~
|
||||
EOC
|
||||
@@ -0,0 +1,8 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts /\\ /\\
|
||||
$thoughts ( $eyes )
|
||||
$thoughts -- $tongue--
|
||||
$thoughts ||||
|
||||
$thoughts ^^^^
|
||||
EOC
|
||||
@@ -0,0 +1,7 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts .
|
||||
$thoughts ,-( $eyes )-.
|
||||
$thoughts '--- $tongue ---'
|
||||
$thoughts Pear / Bare
|
||||
EOC
|
||||
@@ -0,0 +1,9 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts +------+
|
||||
$thoughts | $eyes |
|
||||
$thoughts | $tongue |
|
||||
$thoughts +------+
|
||||
$thoughts ||||
|
||||
$thoughts / \\
|
||||
EOC
|
||||
@@ -0,0 +1,8 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts .--.
|
||||
$thoughts |$eyes|
|
||||
$thoughts _/ $tongue -\\_
|
||||
$thoughts / \\
|
||||
$thoughts /_______\\
|
||||
EOC
|
||||
@@ -0,0 +1,7 @@
|
||||
$the_cow = <<"EOC";
|
||||
$thoughts
|
||||
$thoughts ____
|
||||
$thoughts / $eyes \\
|
||||
$thoughts --' $tongue '--
|
||||
$thoughts \\_______/
|
||||
EOC
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user