This commit is contained in:
Raven Scott
2026-04-03 21:39:36 -04:00
parent 2655fe6a7d
commit 57b1296981
355 changed files with 781062 additions and 7351 deletions
+428
View File
@@ -0,0 +1,428 @@
/* 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
}
/**
* GNU-style LS_COLORS string + dircolors(5) database parsing and file classification.
* Consumed by bare-os-booter (ESM import). Prepended into /bin/ls and /bin/dircolors by bare-os-coreutils build (no import there).
*/
/** Default LS_COLORS (subset aligned with GNU dircolors -p essentials + common globs). */
function bareDefaultLsColorsString() {
return [
'no=00',
'fi=00',
'di=01;34',
'ln=01;36',
'pi=40;33',
'so=01;35',
'do=01;35',
'bd=40;33;01',
'cd=40;33;01',
'or=40;31;01',
'mi=00',
'su=37;41',
'sg=30;43',
'ca=30;41',
'tw=30;42',
'ow=34;43',
'st=37;44',
'ex=01;32',
'mh=00',
'*.tar=01;31',
'*.tgz=01;31',
'*.arc=01;31',
'*.arj=01;31',
'*.taz=01;31',
'*.lha=01;31',
'*.lz4=01;31',
'*.lzh=01;31',
'*.tlz=01;31',
'*.txz=01;31',
'*.tzo=01;31',
'*.tzst=01;31',
'*.bz2=01;31',
'*.bz=01;31',
'*.tbz=01;31',
'*.tbz2=01;31',
'*.tz=01;31',
'*.deb=01;31',
'*.rpm=01;31',
'*.jar=01;31',
'*.war=01;31',
'*.ear=01;31',
'*.sar=01;31',
'*.rar=01;31',
'*.alz=01;31',
'*.ace=01;31',
'*.zoo=01;31',
'*.cpio=01;31',
'*.7z=01;31',
'*.rz=01;31',
'*.cab=01;31',
'*.wim=01;31',
'*.swm=01;31',
'*.dwm=01;31',
'*.esd=01;31',
'*.jpg=01;35',
'*.jpeg=01;35',
'*.mjpg=01;35',
'*.mjpeg=01;35',
'*.gif=01;35',
'*.bmp=01;35',
'*.pbm=01;35',
'*.pgm=01;35',
'*.ppm=01;35',
'*.tga=01;35',
'*.xbm=01;35',
'*.xpm=01;35',
'*.tif=01;35',
'*.tiff=01;35',
'*.png=01;35',
'*.svg=01;35',
'*.svgz=01;35',
'*.mng=01;35',
'*.pcx=01;35',
'*.mov=01;35',
'*.mpg=01;35',
'*.mpeg=01;35',
'*.m2v=01;35',
'*.mkv=01;35',
'*.webm=01;35',
'*.ogm=01;35',
'*.mp4=01;35',
'*.m4v=01;35',
'*.mp4v=01;35',
'*.vob=01;35',
'*.qt=01;35',
'*.nuv=01;35',
'*.wmv=01;35',
'*.asf=01;35',
'*.rm=01;35',
'*.rmvb=01;35',
'*.flc=01;35',
'*.avi=01;35',
'*.fli=01;35',
'*.flv=01;35',
'*.gl=01;35',
'*.dl=01;35',
'*.xcf=01;35',
'*.xwd=01;35',
'*.yuv=01;35',
'*.cgm=01;35',
'*.emf=01;35',
'*.ogv=01;35',
'*.ogx=01;35',
'*.aac=00;36',
'*.au=00;36',
'*.flac=00;36',
'*.m4a=00;36',
'*.mid=00;36',
'*.midi=00;36',
'*.mka=00;36',
'*.mp3=00;36',
'*.mpc=00;36',
'*.ogg=00;36',
'*.ra=00;36',
'*.wav=00;36',
'*.oga=00;36',
'*.opus=00;36',
'*.spx=00;36',
'*.xspf=00;36',
'*.pdf=00;32',
'*.ps=00;32',
'*.txt=00;32',
'*.patch=00;32',
'*.diff=00;32',
'*.log=00;32',
'*.tex=00;32',
'*.doc=00;32',
'*.docx=00;32',
'*.rtf=00;32',
'*.odt=00;32',
'*.md=00;32',
'*.markdown=00;32',
'*.css=00;32',
'*.htm=00;32',
'*.html=00;32',
'*.xml=00;32',
'*.json=00;32',
'*.yaml=00;32',
'*.yml=00;32',
'*.c=00;32',
'*.h=00;32',
'*.js=00;32',
'*.mjs=00;32',
'*.java=00;32',
'*.py=00;32',
'*.go=00;32',
'*.rs=00;32',
'*.cpp=00;32',
'*.cc=00;32',
'*.cxx=00;32',
'*.hpp=00;32',
'*.shlib=01;32',
'*.so=01;32',
'*.dylib=01;32',
'*.dll=01;32',
'*.a=01;32',
'*.lib=01;32',
'*.ko=01;32',
'*.sh=01;32',
'*.bash=01;32',
'*.zsh=01;32',
'*.bat=01;32',
'*.cmd=01;32',
'*.exe=01;32',
'*.com=01;32',
'*.btm=01;32',
'*.msi=01;32'
].join(':')
}
/**
* @param {string} s
* @returns {Record<string, string>}
*/
function bareParseLsColors(s) {
const out = /** @type {Record<string, string>} */ ({})
if (s == null || !String(s).trim()) return out
for (const part of String(s).split(':')) {
const p = part.trim()
if (!p) continue
const eq = p.indexOf('=')
if (eq <= 0) continue
const k = p.slice(0, eq)
const v = p.slice(eq + 1)
if (k) out[k] = v
}
return out
}
/**
* @param {Record<string, string>} map
*/
function bareSerializeLsColors(map) {
const keys = Object.keys(map).sort()
return keys.map((k) => k + '=' + map[k]).join(':')
}
/**
* @param {string} code dircolors value (e.g. 01;34)
* @returns {string} opening SGR including ESC
*/
function bareLscolorsToOpenSgr(code) {
if (code == null || code === '' || code === '0' || code === '00') return ''
return '\x1b[' + code + 'm'
}
/**
* Parse dircolors database (GNU subset): # comments, blank lines, TERM blocks, KEY VALUE pairs.
* @param {string} text
* @param {string} termEnv value of TERM
* @returns {Record<string, string>}
*/
function bareParseDircolorsDatabase(text, termEnv) {
const term = termEnv != null ? String(termEnv) : ''
/** @type {Record<string, string>} */
const out = {}
let blockActive = true
const lines = String(text).split(/\r?\n/)
for (let raw of lines) {
const line = raw.replace(/#.*$/, '').trim()
if (!line) continue
const upper = line.toUpperCase()
if (upper.startsWith('TERM ')) {
const rest = line.slice(5).trim()
if (rest === 'none') blockActive = false
else
blockActive =
rest === '*' || rest === '' || rest === term || term.indexOf(rest) === 0
continue
}
if (upper.startsWith('COLOR ') || upper.startsWith('OPTIONS ')) continue
if (!blockActive) continue
const ws = line.search(/\s/)
if (ws <= 0) continue
const key = line.slice(0, ws).trim()
const val = line.slice(ws + 1).trim()
if (!key || !val) continue
if (key === 'TERM' || key === 'COLOR' || key === 'OPTIONS') continue
out[key] = val
}
return out
}
/**
* @param {string} filename basename only
* @param {Record<string, string>} map
*/
function bareLsColorsGlobMatch(filename, map) {
const keys = Object.keys(map).filter((k) => k.startsWith('*.'))
keys.sort((a, b) => b.length - a.length)
const lower = filename.toLowerCase()
for (const k of keys) {
const suf = k.slice(1).toLowerCase()
if (lower.endsWith(suf)) return map[k]
}
return null
}
/**
* @param {{ type?: string, mode?: number, linkname?: string, targetMissing?: boolean, nlink?: number, capabilities?: boolean } | null} st
* @param {string} filename basename
* @param {Record<string, string>} map from LS_COLORS
* @returns {string} SGR open sequence or ''
*/
function bareLsColorOpenSgrFromMap(st, filename, map) {
if (!st) {
const no = map.no
return no ? bareLscolorsToOpenSgr(no) : ''
}
const mode = Number(st.mode) || 0
const perm = mode & 0o777
const t = st.type
let key = 'fi'
if (t === 'symlink') {
key = st.targetMissing ? 'or' : 'ln'
} else if (t === 'directory') {
const ow = (perm & 0o002) !== 0
const sticky = (mode & 0o1000) !== 0
if (ow && sticky) key = 'tw'
else if (ow) key = 'ow'
else if (sticky) key = 'st'
else key = 'di'
} else if (t === 'file') {
if (st.capabilities) key = 'ca'
else if (mode & 0o4000) key = 'su'
else if (mode & 0o2000) key = 'sg'
else if (perm & 0o111) key = 'ex'
else {
const nlink = Number(st.nlink)
if (nlink > 1 && map.mh != null && String(map.mh).trim() !== '') {
const mhOpen = bareLscolorsToOpenSgr(map.mh)
if (mhOpen) return mhOpen
}
const g = bareLsColorsGlobMatch(filename, map)
if (g != null) return bareLscolorsToOpenSgr(g)
key = 'fi'
}
}
const code = map[key] != null ? map[key] : map.fi
return code ? bareLscolorsToOpenSgr(code) : ''
}
/**
* @returns {string} multiline dircolors database (default colors)
*/
function bareDefaultDircolorsDatabase() {
const pairs = bareParseLsColors(bareDefaultLsColorsString())
const lines = ['# Bare OS default dircolors (GNU-like)', 'TERM *', '']
for (const k of Object.keys(pairs).sort()) {
lines.push(k + ' ' + pairs[k])
}
return lines.join('\n') + '\n'
}
async function run(ctx, argv) {
const env = ctx.env || {}
let bourne = false
let printDefault = false
/** @type {string | null} */
let file = null
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--') continue
if (a === '-b' || a === '--sh' || a === '--bourne-shell') bourne = true
else if (a === '-p' || a === '--print-database') printDefault = true
else if (a.startsWith('-')) {
ctx.console.error('dircolors: unrecognized option: ' + a)
ctx.exitCode = 2
return
} else file = a
}
if (printDefault) {
ctx.console.log(bareDefaultDircolorsDatabase())
return
}
let text = bareDefaultDircolorsDatabase()
if (file) {
const buf = await ctx.vfs.readFile(file)
if (!buf) {
ctx.console.error('dircolors: cannot read ' + file)
ctx.exitCode = 2
return
}
text = ctx.b4a.toString(buf)
}
const term = env.TERM || ''
const db = bareParseDircolorsDatabase(text, term)
const ls = bareSerializeLsColors(db)
if (bourne) {
const q = "'" + ls.replace(/'/g, "'\\''") + "'"
ctx.console.log('LS_COLORS=' + q)
ctx.console.log('export LS_COLORS')
} else {
ctx.console.log(ls)
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ function barePosixBlocks(size) {
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id journalctl jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat systemctl tail tee test time touch tr true tty uname wc wget which whoami xargs'
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname dircolors du echo env exit false find getconf grep head hdms help hostname id journalctl jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat systemctl tail tee test theme time touch tr true tty uname wc wget which whoami xargs'
)
ctx.console.log(
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
+458 -5
View File
@@ -60,10 +60,424 @@ function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* GNU-style LS_COLORS string + dircolors(5) database parsing and file classification.
* Consumed by bare-os-booter (ESM import). Prepended into /bin/ls and /bin/dircolors by bare-os-coreutils build (no import there).
*/
/** Default LS_COLORS (subset aligned with GNU dircolors -p essentials + common globs). */
function bareDefaultLsColorsString() {
return [
'no=00',
'fi=00',
'di=01;34',
'ln=01;36',
'pi=40;33',
'so=01;35',
'do=01;35',
'bd=40;33;01',
'cd=40;33;01',
'or=40;31;01',
'mi=00',
'su=37;41',
'sg=30;43',
'ca=30;41',
'tw=30;42',
'ow=34;43',
'st=37;44',
'ex=01;32',
'mh=00',
'*.tar=01;31',
'*.tgz=01;31',
'*.arc=01;31',
'*.arj=01;31',
'*.taz=01;31',
'*.lha=01;31',
'*.lz4=01;31',
'*.lzh=01;31',
'*.tlz=01;31',
'*.txz=01;31',
'*.tzo=01;31',
'*.tzst=01;31',
'*.bz2=01;31',
'*.bz=01;31',
'*.tbz=01;31',
'*.tbz2=01;31',
'*.tz=01;31',
'*.deb=01;31',
'*.rpm=01;31',
'*.jar=01;31',
'*.war=01;31',
'*.ear=01;31',
'*.sar=01;31',
'*.rar=01;31',
'*.alz=01;31',
'*.ace=01;31',
'*.zoo=01;31',
'*.cpio=01;31',
'*.7z=01;31',
'*.rz=01;31',
'*.cab=01;31',
'*.wim=01;31',
'*.swm=01;31',
'*.dwm=01;31',
'*.esd=01;31',
'*.jpg=01;35',
'*.jpeg=01;35',
'*.mjpg=01;35',
'*.mjpeg=01;35',
'*.gif=01;35',
'*.bmp=01;35',
'*.pbm=01;35',
'*.pgm=01;35',
'*.ppm=01;35',
'*.tga=01;35',
'*.xbm=01;35',
'*.xpm=01;35',
'*.tif=01;35',
'*.tiff=01;35',
'*.png=01;35',
'*.svg=01;35',
'*.svgz=01;35',
'*.mng=01;35',
'*.pcx=01;35',
'*.mov=01;35',
'*.mpg=01;35',
'*.mpeg=01;35',
'*.m2v=01;35',
'*.mkv=01;35',
'*.webm=01;35',
'*.ogm=01;35',
'*.mp4=01;35',
'*.m4v=01;35',
'*.mp4v=01;35',
'*.vob=01;35',
'*.qt=01;35',
'*.nuv=01;35',
'*.wmv=01;35',
'*.asf=01;35',
'*.rm=01;35',
'*.rmvb=01;35',
'*.flc=01;35',
'*.avi=01;35',
'*.fli=01;35',
'*.flv=01;35',
'*.gl=01;35',
'*.dl=01;35',
'*.xcf=01;35',
'*.xwd=01;35',
'*.yuv=01;35',
'*.cgm=01;35',
'*.emf=01;35',
'*.ogv=01;35',
'*.ogx=01;35',
'*.aac=00;36',
'*.au=00;36',
'*.flac=00;36',
'*.m4a=00;36',
'*.mid=00;36',
'*.midi=00;36',
'*.mka=00;36',
'*.mp3=00;36',
'*.mpc=00;36',
'*.ogg=00;36',
'*.ra=00;36',
'*.wav=00;36',
'*.oga=00;36',
'*.opus=00;36',
'*.spx=00;36',
'*.xspf=00;36',
'*.pdf=00;32',
'*.ps=00;32',
'*.txt=00;32',
'*.patch=00;32',
'*.diff=00;32',
'*.log=00;32',
'*.tex=00;32',
'*.doc=00;32',
'*.docx=00;32',
'*.rtf=00;32',
'*.odt=00;32',
'*.md=00;32',
'*.markdown=00;32',
'*.css=00;32',
'*.htm=00;32',
'*.html=00;32',
'*.xml=00;32',
'*.json=00;32',
'*.yaml=00;32',
'*.yml=00;32',
'*.c=00;32',
'*.h=00;32',
'*.js=00;32',
'*.mjs=00;32',
'*.java=00;32',
'*.py=00;32',
'*.go=00;32',
'*.rs=00;32',
'*.cpp=00;32',
'*.cc=00;32',
'*.cxx=00;32',
'*.hpp=00;32',
'*.shlib=01;32',
'*.so=01;32',
'*.dylib=01;32',
'*.dll=01;32',
'*.a=01;32',
'*.lib=01;32',
'*.ko=01;32',
'*.sh=01;32',
'*.bash=01;32',
'*.zsh=01;32',
'*.bat=01;32',
'*.cmd=01;32',
'*.exe=01;32',
'*.com=01;32',
'*.btm=01;32',
'*.msi=01;32'
].join(':')
}
/**
* @param {string} s
* @returns {Record<string, string>}
*/
function bareParseLsColors(s) {
const out = /** @type {Record<string, string>} */ ({})
if (s == null || !String(s).trim()) return out
for (const part of String(s).split(':')) {
const p = part.trim()
if (!p) continue
const eq = p.indexOf('=')
if (eq <= 0) continue
const k = p.slice(0, eq)
const v = p.slice(eq + 1)
if (k) out[k] = v
}
return out
}
/**
* @param {Record<string, string>} map
*/
function bareSerializeLsColors(map) {
const keys = Object.keys(map).sort()
return keys.map((k) => k + '=' + map[k]).join(':')
}
/**
* @param {string} code dircolors value (e.g. 01;34)
* @returns {string} opening SGR including ESC
*/
function bareLscolorsToOpenSgr(code) {
if (code == null || code === '' || code === '0' || code === '00') return ''
return '\x1b[' + code + 'm'
}
/**
* Parse dircolors database (GNU subset): # comments, blank lines, TERM blocks, KEY VALUE pairs.
* @param {string} text
* @param {string} termEnv value of TERM
* @returns {Record<string, string>}
*/
function bareParseDircolorsDatabase(text, termEnv) {
const term = termEnv != null ? String(termEnv) : ''
/** @type {Record<string, string>} */
const out = {}
let blockActive = true
const lines = String(text).split(/\r?\n/)
for (let raw of lines) {
const line = raw.replace(/#.*$/, '').trim()
if (!line) continue
const upper = line.toUpperCase()
if (upper.startsWith('TERM ')) {
const rest = line.slice(5).trim()
if (rest === 'none') blockActive = false
else
blockActive =
rest === '*' || rest === '' || rest === term || term.indexOf(rest) === 0
continue
}
if (upper.startsWith('COLOR ') || upper.startsWith('OPTIONS ')) continue
if (!blockActive) continue
const ws = line.search(/\s/)
if (ws <= 0) continue
const key = line.slice(0, ws).trim()
const val = line.slice(ws + 1).trim()
if (!key || !val) continue
if (key === 'TERM' || key === 'COLOR' || key === 'OPTIONS') continue
out[key] = val
}
return out
}
/**
* @param {string} filename basename only
* @param {Record<string, string>} map
*/
function bareLsColorsGlobMatch(filename, map) {
const keys = Object.keys(map).filter((k) => k.startsWith('*.'))
keys.sort((a, b) => b.length - a.length)
const lower = filename.toLowerCase()
for (const k of keys) {
const suf = k.slice(1).toLowerCase()
if (lower.endsWith(suf)) return map[k]
}
return null
}
/**
* @param {{ type?: string, mode?: number, linkname?: string, targetMissing?: boolean, nlink?: number, capabilities?: boolean } | null} st
* @param {string} filename basename
* @param {Record<string, string>} map from LS_COLORS
* @returns {string} SGR open sequence or ''
*/
function bareLsColorOpenSgrFromMap(st, filename, map) {
if (!st) {
const no = map.no
return no ? bareLscolorsToOpenSgr(no) : ''
}
const mode = Number(st.mode) || 0
const perm = mode & 0o777
const t = st.type
let key = 'fi'
if (t === 'symlink') {
key = st.targetMissing ? 'or' : 'ln'
} else if (t === 'directory') {
const ow = (perm & 0o002) !== 0
const sticky = (mode & 0o1000) !== 0
if (ow && sticky) key = 'tw'
else if (ow) key = 'ow'
else if (sticky) key = 'st'
else key = 'di'
} else if (t === 'file') {
if (st.capabilities) key = 'ca'
else if (mode & 0o4000) key = 'su'
else if (mode & 0o2000) key = 'sg'
else if (perm & 0o111) key = 'ex'
else {
const nlink = Number(st.nlink)
if (nlink > 1 && map.mh != null && String(map.mh).trim() !== '') {
const mhOpen = bareLscolorsToOpenSgr(map.mh)
if (mhOpen) return mhOpen
}
const g = bareLsColorsGlobMatch(filename, map)
if (g != null) return bareLscolorsToOpenSgr(g)
key = 'fi'
}
}
const code = map[key] != null ? map[key] : map.fi
return code ? bareLscolorsToOpenSgr(code) : ''
}
/**
* @returns {string} multiline dircolors database (default colors)
*/
function bareDefaultDircolorsDatabase() {
const pairs = bareParseLsColors(bareDefaultLsColorsString())
const lines = ['# Bare OS default dircolors (GNU-like)', 'TERM *', '']
for (const k of Object.keys(pairs).sort()) {
lines.push(k + ' ' + pairs[k])
}
return lines.join('\n') + '\n'
}
/** ANSI coloring for /bin/ls (after bare-os-lscolors.js preamble). */
const BARE_LS_RESET = '\x1b[0m'
/**
* @param {Record<string, unknown>} [ctx]
* @returns {import('stream').Writable | undefined}
*/
function bareLsResolveStdout(ctx) {
if (!ctx || typeof ctx !== 'object') return globalThis.process?.stdout
const c = /** @type {{ replStdout?: unknown, stdout?: unknown }} */ (ctx)
const out = c.replStdout || c.stdout || globalThis.process?.stdout
return /** @type {import('stream').Writable | undefined} */ (out)
}
/**
* @param {Record<string, unknown>} [ctx]
* @param {'never' | 'auto' | 'always'} colorMode
*/
function bareLsUseColor(ctx, colorMode) {
const env =
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false
if (colorMode === 'never') return false
if (colorMode === 'always') return true
const out = bareLsResolveStdout(ctx)
return Boolean(out && /** @type {{ isTTY?: boolean }} */ (out).isTTY)
}
/**
* @param {Record<string, unknown>} [ctx]
* @returns {Record<string, string>}
*/
function bareLsGetLsColorsMap(ctx) {
const env =
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const raw = env.LS_COLORS
if (raw == null || !String(raw).trim()) {
return bareParseLsColors(bareDefaultLsColorsString())
}
return bareParseLsColors(String(raw))
}
/**
* @param {{ mode?: number, type?: string, linkname?: string, targetMissing?: boolean, nlink?: number, capabilities?: boolean } | null | undefined} st
* @param {string} filename basename
* @param {Record<string, unknown>} [ctx]
*/
function bareLsOpenSgr(st, filename, ctx) {
const map = bareLsGetLsColorsMap(ctx)
return bareLsColorOpenSgrFromMap(st, filename, map)
}
/**
* @param {string} name
* @param {{ mode?: number, type?: string } | null | undefined} st
* @param {boolean} on
* @param {Record<string, unknown>} [ctx]
*/
function bareLsColorWrap(name, st, on, ctx) {
if (!on) return name
const open = bareLsOpenSgr(st, name, ctx)
if (!open) return name
return open + name + BARE_LS_RESET
}
/**
* @param {string} name
* @param {string} arrow
* @param {{ mode?: number, type?: string, linkname?: string, targetMissing?: boolean, nlink?: number, capabilities?: boolean } | null | undefined} st
* @param {boolean} on
* @param {Record<string, unknown>} [ctx]
*/
function bareLsColorLongTail(name, arrow, st, on, ctx) {
if (!on) return name + arrow
if (st && st.type === 'symlink') {
const map = bareLsGetLsColorsMap(ctx)
const ln = map.ln
const open = ln ? bareLscolorsToOpenSgr(ln) : '\x1b[36m'
return open + name + arrow + BARE_LS_RESET
}
return bareLsColorWrap(name, st, true, ctx) + arrow
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let showAll = false
let longFmt = false
/** @type {'never' | 'auto' | 'always'} */
let colorMode = 'auto'
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
@@ -71,6 +485,22 @@ async function run(ctx, argv) {
paths.push(...argv.slice(i + 1))
break
}
if (a.startsWith('--')) {
if (a === '--color') {
colorMode = 'auto'
continue
}
if (a.startsWith('--color=')) {
const v = a.slice(8).toLowerCase()
if (v === 'never' || v === 'none' || v === 'no') colorMode = 'never'
else if (v === 'always' || v === 'yes' || v === 'force') colorMode = 'always'
else colorMode = 'auto'
continue
}
ctx.console.error('ls: unrecognized option ' + a)
ctx.exitCode = 2
return
}
if (a.startsWith('-') && a.length > 1) {
for (let j = 1; j < a.length; j++) {
const c = a[j]
@@ -83,6 +513,7 @@ async function run(ctx, argv) {
paths.push(a)
}
const targets = paths.length ? paths : ['.']
const useColor = bareLsUseColor(ctx, colorMode)
for (const t of targets) {
if (targets.length > 1) ctx.console.log(t + ':')
@@ -104,7 +535,27 @@ async function run(ctx, argv) {
}
if (!showAll) names = names.filter((n) => !n.startsWith('.'))
if (!longFmt) {
ctx.console.log(names.join(' '))
if (!useColor) {
ctx.console.log(names.join(' '))
} else {
const parts = []
for (const n of names) {
const sub =
singleEntryPath != null
? singleEntryPath
: t === '.' || t === './'
? n
: t.replace(/\/$/, '') + '/' + n
let st = null
try {
st = await vfs.lstat(sub)
} catch {
st = null
}
parts.push(bareLsColorWrap(n, st, true, ctx))
}
ctx.console.log(parts.join(' '))
}
} else {
let totalBlocks = 0
const rows = []
@@ -125,7 +576,8 @@ async function run(ctx, argv) {
size: 0,
mtimeStr: '?',
name: n,
arrow: ''
arrow: '',
st: null
})
continue
}
@@ -151,11 +603,13 @@ async function run(ctx, argv) {
size,
mtimeStr,
name: n,
arrow
arrow,
st
})
}
if (singleEntryPath == null) ctx.console.log('total ' + totalBlocks)
for (const r of rows) {
const tail = bareLsColorLongTail(r.name, r.arrow, r.st, useColor, ctx)
ctx.console.log(
r.modeStr +
' ' +
@@ -169,8 +623,7 @@ async function run(ctx, argv) {
' ' +
r.mtimeStr +
' ' +
r.name +
r.arrow
tail
)
}
}
+196
View File
@@ -0,0 +1,196 @@
/* 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
}
/**
* Theme switcher: list presets, show current, set preset (updates ~/.barerc), re-apply.
* Uses ctx.bareOsListThemes / ctx.bareOsApplyTheme when provided by the booter.
*/
const FALLBACK_THEMES = [
'catppuccin_mocha',
'default',
'dracula',
'github_dark',
'gruvbox_dark',
'nord',
'solarized_dark',
'tokyo_night'
]
function themeListFromCtx(ctx) {
if (typeof ctx.bareOsListThemes === 'function') {
try {
const names = ctx.bareOsListThemes()
if (Array.isArray(names) && names.length) return names
} catch {
/* fall through */
}
}
return FALLBACK_THEMES.slice().sort()
}
async function readBarerc(ctx) {
try {
const buf = await ctx.vfs.readFile('~/.barerc')
return buf ? ctx.b4a.toString(buf) : ''
} catch {
return ''
}
}
async function writeBarerc(ctx, text) {
await ctx.vfs.writeFile('~/.barerc', ctx.b4a.from(text))
}
/**
* Ensure ~/.barerc contains `theme <name>` (replace first theme line or append).
* @param {Record<string, unknown>} ctx
* @param {string} name
*/
async function persistThemeLine(ctx, name) {
let text = await readBarerc(ctx)
const lines = text.split(/\r?\n/)
const themeRe = /^\s*theme\s+/
let replaced = false
const out = []
for (const line of lines) {
if (themeRe.test(line)) {
if (!replaced) {
out.push('theme ' + name)
replaced = true
}
} else out.push(line)
}
if (!replaced) {
if (out.length && out[out.length - 1].trim() !== '') out.push('')
out.push('theme ' + name)
}
const newText = out.join('\n')
if (!/\n$/.test(newText) && newText.length) await writeBarerc(ctx, newText + '\n')
else await writeBarerc(ctx, newText || 'theme ' + name + '\n')
}
async function run(ctx, argv) {
const env = ctx.vfs?.env || ctx.env || {}
const sub = argv[1] || 'list'
if (sub === 'list') {
for (const n of themeListFromCtx(ctx)) ctx.console.log(n)
return
}
if (sub === 'current') {
const t = env.BARE_OS_THEME != null ? String(env.BARE_OS_THEME) : 'default'
ctx.console.log(t)
const ls = env.LS_COLORS
if (ls != null && String(ls).length) {
ctx.console.log('LS_COLORS_len=' + String(ls).length)
}
return
}
if (sub === 'apply') {
if (typeof ctx.bareOsApplyTheme === 'function') {
await ctx.bareOsApplyTheme()
} else {
ctx.console.error('theme: bareOsApplyTheme not available')
ctx.exitCode = 1
}
return
}
if (sub === 'set') {
const name = argv[2]
if (!name) {
ctx.console.error('theme: usage: theme set <name>')
ctx.exitCode = 1
return
}
const norm = String(name)
.toLowerCase()
.trim()
.replace(/\s+/g, '_')
const valid = new Set(themeListFromCtx(ctx).map((s) => s.toLowerCase()))
if (!valid.has(norm)) {
ctx.console.error('theme: unknown preset: ' + name)
ctx.exitCode = 1
return
}
try {
await persistThemeLine(ctx, norm)
} catch (e) {
ctx.console.error('theme: ' + ((e && e.message) || e))
ctx.exitCode = 2
return
}
env.BARE_OS_THEME = norm
if (typeof ctx.bareOsApplyTheme === 'function') {
await ctx.bareOsApplyTheme()
}
return
}
ctx.console.error(
'theme: usage: theme [list|current|set <name>|apply]'
)
ctx.exitCode = 1
}