mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-07 05:54:06 +09:00
doc update/command synopses
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -74,3 +74,6 @@ assets/disk0/movtestimg/*.jpg
|
||||
assets/disk0/*.mov
|
||||
assets/diskMediabin/*
|
||||
assets/disk0/hopper/*
|
||||
|
||||
# TVDOS runtime caches (regenerated on the VM; never commit)
|
||||
assets/disk0/tvdos/cache/
|
||||
|
||||
16
assets/disk0/tvdos/bin/color.js.synopsis
Normal file
16
assets/disk0/tvdos/bin/color.js.synopsis
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "color",
|
||||
"summary": "Set the screen background and foreground colours",
|
||||
"symbols": {
|
||||
"code": {
|
||||
"kind": "positional",
|
||||
"type": "string",
|
||||
"name": "BF",
|
||||
"summary": "Two hex digits: background then foreground",
|
||||
"validation": { "pattern": "^[0-9A-Fa-f]{2}$" },
|
||||
"completion": { "method": "none" }
|
||||
}
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "code" }
|
||||
}
|
||||
@@ -1013,6 +1013,22 @@ function getAutocompleteWin() {
|
||||
return _acWin
|
||||
}
|
||||
|
||||
// Lazily-resolved synopsis module (TSF loader/completion resolver). Held for
|
||||
// the whole session so its in-memory cache survives across keystrokes.
|
||||
// undefined = not probed yet, null = unavailable.
|
||||
let _acSyn = undefined
|
||||
function getSynopsisMod() {
|
||||
if (_acSyn !== undefined) return _acSyn
|
||||
_acSyn = null
|
||||
try {
|
||||
let m = require("synopsis") // resolved through INCLPATH (\tvdos\include\synopsis.mjs)
|
||||
if (m && typeof m.getCompletion === "function") _acSyn = m
|
||||
} catch (e) {
|
||||
debugprintln("command.js > autocomplete: synopsis unavailable: " + e)
|
||||
}
|
||||
return _acSyn
|
||||
}
|
||||
|
||||
// List a directory's entries, swallowing any IO error.
|
||||
function _acListDir(fullPath) {
|
||||
try {
|
||||
@@ -1103,15 +1119,53 @@ function _acPathCandidates(word) {
|
||||
return out
|
||||
}
|
||||
|
||||
// Candidates for an argument (not the command word). Consults the command's
|
||||
// TSF synopsis (via synopsis.mjs) for option flags, enum/list values and
|
||||
// subcommand names, and merges in filesystem entries when the synopsis says the
|
||||
// slot expects a path/file/directory. Falls back to plain path completion when
|
||||
// no synopsis exists, so behaviour is unchanged for commands without one.
|
||||
function _acArgCandidates(prefix, word) {
|
||||
let syn = getSynopsisMod()
|
||||
if (syn) {
|
||||
try {
|
||||
let toks = prefix.trim().split(/\s+/)
|
||||
let cmd = toks[0]
|
||||
let argToks = toks.slice(1)
|
||||
let r = syn.getCompletion(cmd, argToks, word)
|
||||
if (r && r.ok) {
|
||||
let out = (r.candidates || []).slice()
|
||||
if (r.filesystem) {
|
||||
_acPathCandidates(word).forEach(function(c) {
|
||||
if (r.filesystem === 'directory' && !c.isDir) return // dirs only
|
||||
out.push(c)
|
||||
})
|
||||
}
|
||||
// de-dupe by the text that would be inserted
|
||||
let seen = {}, dedup = []
|
||||
out.forEach(function(c) { if (seen[c.value]) return; seen[c.value] = true; dedup.push(c) })
|
||||
return dedup
|
||||
}
|
||||
} catch (e) {
|
||||
debugprintln("command.js > _acArgCandidates: " + e)
|
||||
}
|
||||
}
|
||||
return _acPathCandidates(word)
|
||||
}
|
||||
|
||||
// Work out what is being completed at `caret` within `line`.
|
||||
// Returns { wordStart, word, candidates } (candidates sorted by label).
|
||||
function computeCompletion(line, caret) {
|
||||
let wordStart = caret
|
||||
while (wordStart > 0 && line.charAt(wordStart - 1) !== ' ') wordStart -= 1
|
||||
let word = line.substring(wordStart, caret)
|
||||
let isFirstWord = (line.substring(0, wordStart).trim().length === 0)
|
||||
let prefix = line.substring(0, wordStart)
|
||||
let isFirstWord = (prefix.trim().length === 0)
|
||||
let hasPathSep = (word.indexOf('\\') >= 0 || word.indexOf('/') >= 0 || word.indexOf(':') >= 0)
|
||||
let candidates = (isFirstWord && !hasPathSep) ? _acCommandCandidates(word) : _acPathCandidates(word)
|
||||
let candidates
|
||||
if (isFirstWord)
|
||||
candidates = hasPathSep ? _acPathCandidates(word) : _acCommandCandidates(word)
|
||||
else
|
||||
candidates = _acArgCandidates(prefix, word)
|
||||
candidates.sort(function(a, b) { return (a.label < b.label) ? -1 : (a.label > b.label) ? 1 : 0 })
|
||||
return { wordStart: wordStart, word: word, candidates: candidates }
|
||||
}
|
||||
|
||||
7
assets/disk0/tvdos/bin/drives.js.synopsis
Normal file
7
assets/disk0/tvdos/bin/drives.js.synopsis
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "drives",
|
||||
"summary": "List connected and mounted disk drives",
|
||||
"symbols": {},
|
||||
"synopsis": { "type": "sequence", "children": [] }
|
||||
}
|
||||
12
assets/disk0/tvdos/bin/edit.js.synopsis
Normal file
12
assets/disk0/tvdos/bin/edit.js.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "edit",
|
||||
"summary": "Full-screen text editor",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "File to edit; a new buffer when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
}
|
||||
9
assets/disk0/tvdos/bin/geturl.js.synopsis
Normal file
9
assets/disk0/tvdos/bin/geturl.js.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "geturl",
|
||||
"summary": "Fetch a URL and print the response",
|
||||
"symbols": {
|
||||
"url": { "kind": "positional", "type": "url", "name": "URL", "summary": "Address to fetch" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "url" }
|
||||
}
|
||||
18
assets/disk0/tvdos/bin/gzip.js.synopsis
Normal file
18
assets/disk0/tvdos/bin/gzip.js.synopsis
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "gzip",
|
||||
"summary": "Compress or decompress a file (Zstd-backed)",
|
||||
"symbols": {
|
||||
"decompress": { "kind": "option", "short": "-d", "summary": "Decompress instead of compress" },
|
||||
"stdout": { "kind": "option", "short": "-c", "summary": "Write to the pipe instead of a file" },
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["decompress", "stdout"] },
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "File to process" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } },
|
||||
{ "type": "reference", "symbol": "file" }
|
||||
]
|
||||
}
|
||||
}
|
||||
1
assets/disk0/tvdos/bin/help.alias
Normal file
1
assets/disk0/tvdos/bin/help.alias
Normal file
@@ -0,0 +1 @@
|
||||
synopsis $0
|
||||
12
assets/disk0/tvdos/bin/hexdump.js.synopsis
Normal file
12
assets/disk0/tvdos/bin/hexdump.js.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "hexdump",
|
||||
"summary": "Print a file as a hexadecimal dump",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "File to dump; reads from the pipe when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
}
|
||||
12
assets/disk0/tvdos/bin/less.js.synopsis
Normal file
12
assets/disk0/tvdos/bin/less.js.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "less",
|
||||
"summary": "View text a screen at a time",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "File to view; reads from the pipe when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
}
|
||||
34
assets/disk0/tvdos/bin/lfs.js.synopsis
Normal file
34
assets/disk0/tvdos/bin/lfs.js.synopsis
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "lfs",
|
||||
"summary": "Create, extract or list a Linear File Strip (.lfs) archive",
|
||||
"description": "Bundles a directory tree into a single TVDOS Linear File Strip archive, or unpacks one. Exactly one mode must be given: -c creates ARCHIVE from PATH, -x extracts ARCHIVE into PATH, and -t lists the files in ARCHIVE (PATH is not used). Individual files are stored uncompressed; gzip the whole .lfs to compress it.",
|
||||
"symbols": {
|
||||
"create": { "kind": "option", "short": "-c", "summary": "Create an archive from a directory" },
|
||||
"extract": { "kind": "option", "short": "-x", "summary": "Extract an archive into a directory" },
|
||||
"list": { "kind": "option", "short": "-t", "summary": "List the files stored in an archive" },
|
||||
"relative": { "kind": "option", "short": "-r", "summary": "Store paths relative to the source directory (with -c)" },
|
||||
"archive": { "kind": "positional", "type": "file", "name": "ARCHIVE", "summary": "The .lfs archive file" },
|
||||
"path": { "kind": "positional", "type": "directory", "name": "PATH", "summary": "Source directory (-c) or destination directory (-x); unused for -t" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "choice",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "create" },
|
||||
{ "type": "reference", "symbol": "extract" },
|
||||
{ "type": "reference", "symbol": "list" }
|
||||
]
|
||||
},
|
||||
{ "type": "optional", "child": { "type": "reference", "symbol": "relative" } },
|
||||
{ "type": "reference", "symbol": "archive" },
|
||||
{ "type": "optional", "child": { "type": "reference", "symbol": "path" } }
|
||||
]
|
||||
},
|
||||
"constraints": [
|
||||
{ "type": "cardinality", "symbols": ["create", "extract", "list"], "minimum": 1, "maximum": 1 },
|
||||
{ "type": "requires", "subject": "relative", "targets": ["create"] }
|
||||
]
|
||||
}
|
||||
9
assets/disk0/tvdos/bin/movprobe.js.synopsis
Normal file
9
assets/disk0/tvdos/bin/movprobe.js.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "movprobe",
|
||||
"summary": "Print metadata about a movie file",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "Movie file to inspect" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
17
assets/disk0/tvdos/bin/playmp2.js.synopsis
Normal file
17
assets/disk0/tvdos/bin/playmp2.js.synopsis
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "playmp2",
|
||||
"summary": "Play an MP2 audio file",
|
||||
"symbols": {
|
||||
"interactive": { "kind": "option", "short": "-i", "summary": "Interactive mode (visualiser)" },
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["interactive"] },
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "MP2 file to play" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "file" },
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
21
assets/disk0/tvdos/bin/playtad.js.synopsis
Normal file
21
assets/disk0/tvdos/bin/playtad.js.synopsis
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "playtad",
|
||||
"summary": "Play a TAD audio file",
|
||||
"symbols": {
|
||||
"interactive": { "kind": "option", "short": "-i", "summary": "Interactive mode (visualiser and progress bar)" },
|
||||
"dump": { "kind": "option", "short": "-d", "summary": "Dump coefficients (diagnostic)" },
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["interactive", "dump"] },
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "TAD file to play" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "file" },
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } }
|
||||
]
|
||||
},
|
||||
"constraints": [
|
||||
{ "type": "conflicts", "symbols": ["interactive", "dump"] }
|
||||
]
|
||||
}
|
||||
23
assets/disk0/tvdos/bin/playtav.js.synopsis
Normal file
23
assets/disk0/tvdos/bin/playtav.js.synopsis
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "playtav",
|
||||
"summary": "Play a TAV video file",
|
||||
"symbols": {
|
||||
"interactive": { "kind": "option", "short": "-i", "summary": "Interactive mode" },
|
||||
"filmGrain": {
|
||||
"kind": "option",
|
||||
"long": "--filter-film-grain",
|
||||
"summary": "Apply a film-grain filter",
|
||||
"value": { "name": "LEVEL", "type": "integer", "required": false, "summary": "Grain intensity" }
|
||||
},
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["interactive", "filmGrain"] },
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "TAV file to play" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "file" },
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
18
assets/disk0/tvdos/bin/playtev.js.synopsis
Normal file
18
assets/disk0/tvdos/bin/playtev.js.synopsis
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "playtev",
|
||||
"summary": "Play a TEV video file",
|
||||
"symbols": {
|
||||
"interactive": { "kind": "option", "short": "-i", "summary": "Interactive mode" },
|
||||
"debugMv": { "kind": "option", "long": "-debug-mv", "summary": "Show motion-vector debug overlay" },
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["interactive", "debugMv"] },
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "TEV file to play" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "file" },
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
17
assets/disk0/tvdos/bin/playwav.js.synopsis
Normal file
17
assets/disk0/tvdos/bin/playwav.js.synopsis
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "playwav",
|
||||
"summary": "Play a WAV audio file",
|
||||
"symbols": {
|
||||
"interactive": { "kind": "option", "short": "-i", "summary": "Interactive mode (visualiser)" },
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["interactive"] },
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "WAV file to play" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "file" },
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
9
assets/disk0/tvdos/bin/printfile.js.synopsis
Normal file
9
assets/disk0/tvdos/bin/printfile.js.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "printfile",
|
||||
"summary": "Print a text file with line numbers",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "Text file to print" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
180
assets/disk0/tvdos/bin/synopsis.js
Normal file
180
assets/disk0/tvdos/bin/synopsis.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* synopsis.js -- system-wide help / tldr.
|
||||
*
|
||||
* Prints a command's human-targeted one-line summary and an auto-generated
|
||||
* synopsis (usage line, arguments, options and constraints) derived from its
|
||||
* TSF .synopsis document via the `synopsis` library (synopsis.mjs).
|
||||
*
|
||||
* Usage: synopsis PROGRAM
|
||||
* synopsis (describes itself)
|
||||
*/
|
||||
|
||||
let syn
|
||||
try {
|
||||
syn = require("synopsis")
|
||||
} catch (e) {
|
||||
printerrln("synopsis: the 'synopsis' library is not installed")
|
||||
return 1
|
||||
}
|
||||
|
||||
const termW = (con.getmaxyx()[1]) || 80
|
||||
|
||||
// Word-wrap plain text to `width`, returning an array of lines.
|
||||
function wrap(text, width) {
|
||||
if (!text) return []
|
||||
if (width < 8) width = 8
|
||||
let words = ('' + text).split(/\s+/).filter(function (w) { return w.length })
|
||||
let lines = [], line = ''
|
||||
words.forEach(function (w) {
|
||||
if (line.length === 0) line = w
|
||||
else if (line.length + 1 + w.length <= width) line += ' ' + w
|
||||
else { lines.push(line); line = w }
|
||||
})
|
||||
if (line.length) lines.push(line)
|
||||
return lines
|
||||
}
|
||||
|
||||
// Print a "left summary" row: the summary is wrapped into the right column and
|
||||
// continuation lines are aligned under it. An over-wide `left` spills onto its
|
||||
// own line.
|
||||
function row(left, summary, leftW, indent) {
|
||||
let pad = ' '.repeat(indent)
|
||||
let gap = 2
|
||||
let sumW = Math.max(8, termW - indent - leftW - gap)
|
||||
let wrapped = wrap(summary, sumW)
|
||||
if (left.length > leftW) {
|
||||
println(pad + left)
|
||||
wrapped.forEach(function (l) { println(pad + ' '.repeat(leftW + gap) + l) })
|
||||
} else {
|
||||
let first = wrapped.length ? wrapped[0] : ''
|
||||
println(pad + left + ' '.repeat(leftW - left.length + gap) + first)
|
||||
for (let i = 1; i < wrapped.length; i++)
|
||||
println(pad + ' '.repeat(leftW + gap) + wrapped[i])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- resolve the target ----------------------------------------------------
|
||||
let token = (exec_args[1] !== undefined && exec_args[1] !== '') ? exec_args[1] : "synopsis"
|
||||
|
||||
let model = syn.getModel(token)
|
||||
if (!model) {
|
||||
printerrln(`synopsis: no synopsis found for '${token}'`)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Display name for a referenced symbol id (used by the constraints section).
|
||||
function symDisplay(id) {
|
||||
let s = model.symbols[id]
|
||||
if (!s) return id
|
||||
if (s.kind === 'option') return s.long || s.short || id
|
||||
if (s.kind === 'positional') return s.name || id
|
||||
if (s.kind === 'subcommand') return s.name || id
|
||||
return id
|
||||
}
|
||||
|
||||
// Append a "{a, b, c}" hint of permitted values to a summary, if any.
|
||||
function withValues(summary, values) {
|
||||
if (!values || !values.length) return summary || ''
|
||||
let vs = values.map(function (v) {
|
||||
return (v && typeof v === 'object' && ('value' in v)) ? v.value : v
|
||||
}).join(', ')
|
||||
return (summary ? summary + ' ' : '') + '{' + vs + '}'
|
||||
}
|
||||
|
||||
// Left-column text for an option, e.g. "-o, --output=FILE".
|
||||
function optionLeft(e) {
|
||||
let forms = []
|
||||
if (e.short) forms.push(e.short)
|
||||
if (e.long) forms.push(e.long)
|
||||
let s = forms.join(', ')
|
||||
if (e.hasValue) {
|
||||
let vn = (e.value && (e.value.name || e.value.type)) || 'VALUE'
|
||||
if (e.long) s += e.valueRequired ? '=' + vn : '[=' + vn + ']'
|
||||
else s += e.valueRequired ? ' ' + vn : ' [' + vn + ']'
|
||||
}
|
||||
return s
|
||||
}
|
||||
function optionSummary(e) {
|
||||
let s = e.summary || ''
|
||||
if (e.negatable) s += (s ? ' ' : '') + '(negatable)'
|
||||
if (e.value && e.value.values && e.value.values.length) s = withValues(s, e.value.values)
|
||||
return s
|
||||
}
|
||||
|
||||
function constraintText(c) {
|
||||
let names = (c.symbols || []).map(symDisplay)
|
||||
if (c.type === 'conflicts') return 'Mutually exclusive: ' + names.join(', ')
|
||||
if (c.type === 'requires') return symDisplay(c.subject) + ' requires ' + (c.targets || []).map(symDisplay).join(', ')
|
||||
if (c.type === 'implies') return symDisplay(c.subject) + ' implies ' + (c.targets || []).map(symDisplay).join(', ')
|
||||
if (c.type === 'cardinality') {
|
||||
let mn = c.minimum, mx = c.maximum, q
|
||||
if (mn === 1 && mx === 1) q = 'Exactly one of'
|
||||
else if (mn === 1 && mx === undefined) q = 'At least one of'
|
||||
else if (mn === undefined && mx === 1) q = 'At most one of'
|
||||
else q = `Between ${mn} and ${mx} of`
|
||||
return q + ': ' + names.join(', ')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ---- gather rows -----------------------------------------------------------
|
||||
let argEntries = model.positionals.map(function (p) {
|
||||
return { left: (p.name || p.id) + (p.repeatable ? '...' : ''), summary: withValues(p.summary, p.values) }
|
||||
})
|
||||
let optEntries = model.flags.map(function (e) {
|
||||
return { left: optionLeft(e), summary: optionSummary(e) }
|
||||
})
|
||||
let subEntries = model.subcommands.map(function (s) {
|
||||
return { left: s.name, summary: s.summary || '' }
|
||||
})
|
||||
|
||||
// shared left-column width (capped so a long flag does not push everything out)
|
||||
let leftW = 4
|
||||
argEntries.concat(optEntries, subEntries).forEach(function (e) { if (e.left.length > leftW) leftW = e.left.length })
|
||||
if (leftW > 30) leftW = 30
|
||||
|
||||
// ---- render ----------------------------------------------------------------
|
||||
let title = model.name || token
|
||||
println(model.summary ? `${title} - ${model.summary}` : title)
|
||||
println()
|
||||
|
||||
let usage = syn.getUsage(token)
|
||||
if (usage) {
|
||||
println("Usage:")
|
||||
println(" " + usage)
|
||||
println()
|
||||
}
|
||||
|
||||
if (model.description) {
|
||||
wrap(model.description, termW).forEach(function (l) { println(l) })
|
||||
println()
|
||||
}
|
||||
|
||||
if (subEntries.length) {
|
||||
println("Commands:")
|
||||
subEntries.forEach(function (e) { row(e.left, e.summary, leftW, 4) })
|
||||
println()
|
||||
}
|
||||
|
||||
if (argEntries.length) {
|
||||
println("Arguments:")
|
||||
argEntries.forEach(function (e) { row(e.left, e.summary, leftW, 4) })
|
||||
println()
|
||||
}
|
||||
|
||||
if (optEntries.length) {
|
||||
println("Options:")
|
||||
optEntries.forEach(function (e) { row(e.left, e.summary, leftW, 4) })
|
||||
println()
|
||||
}
|
||||
|
||||
if (model.constraints && model.constraints.length) {
|
||||
let lines = model.constraints.map(constraintText).filter(function (t) { return t })
|
||||
if (lines.length) {
|
||||
println("Constraints:")
|
||||
lines.forEach(function (l) { println(" " + l) })
|
||||
println()
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
13
assets/disk0/tvdos/bin/synopsis.js.synopsis
Normal file
13
assets/disk0/tvdos/bin/synopsis.js.synopsis
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "synopsis",
|
||||
"summary": "Print a command's summary and auto-generated synopsis",
|
||||
"description": "Prints the one-line summary and an auto-generated usage line for PROGRAM, derived from its TSF .synopsis document, together with its arguments, options and constraints. With no PROGRAM, describes itself.",
|
||||
"symbols": {
|
||||
"program": { "kind": "positional", "type": "command", "name": "PROGRAM", "summary": "Command to describe; describes synopsis itself when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "program" }
|
||||
}
|
||||
}
|
||||
9
assets/disk0/tvdos/bin/tee.js.synopsis
Normal file
9
assets/disk0/tvdos/bin/tee.js.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "tee",
|
||||
"summary": "Copy a pipe's stream to a file and pass it on",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "path", "name": "FILE", "summary": "File to write the stream to" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
17
assets/disk0/tvdos/bin/touch.js.synopsis
Normal file
17
assets/disk0/tvdos/bin/touch.js.synopsis
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "touch",
|
||||
"summary": "Update a file's modification time, creating it if absent",
|
||||
"symbols": {
|
||||
"noCreate": { "kind": "option", "short": "-c", "summary": "Do not create the file if it does not exist" },
|
||||
"options": { "kind": "group", "summary": "Options", "members": ["noCreate"] },
|
||||
"file": { "kind": "positional", "type": "path", "name": "FILE", "summary": "File to touch" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } },
|
||||
{ "type": "reference", "symbol": "file" }
|
||||
]
|
||||
}
|
||||
}
|
||||
9
assets/disk0/tvdos/bin/writeto.js.synopsis
Normal file
9
assets/disk0/tvdos/bin/writeto.js.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "writeto",
|
||||
"summary": "Write a pipe's stream to a file",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "path", "name": "FILE", "summary": "File to write the stream to" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
581
assets/disk0/tvdos/include/synopsis.mjs
Normal file
581
assets/disk0/tvdos/include/synopsis.mjs
Normal file
@@ -0,0 +1,581 @@
|
||||
/*
|
||||
* synopsis.mjs -- TVDOS Synopsis Format (TSF) loader, cache and completion
|
||||
* resolver.
|
||||
*
|
||||
* A TSF document (see the "Command Synopsis Format" chapter of the manual and
|
||||
* tvdos_synopsis_format_draft.md) is a JSON file describing a command's
|
||||
* command-line interface: its options, positional arguments, subcommands,
|
||||
* argument types, completion sources and validation constraints. This module
|
||||
* turns those documents into the answers command.js needs while the user is
|
||||
* typing -- chiefly "what can come next at the caret?".
|
||||
*
|
||||
* Where the documents live
|
||||
* ------------------------
|
||||
* * Apps : colocated with the executable, full filename + ".synopsis"
|
||||
* e.g. \tvdos\bin\geturl.js -> \tvdos\bin\geturl.js.synopsis
|
||||
* * Built-in : the shell coreutils are not files, so their synopses live
|
||||
* coreutils in a dedicated directory, \tvdos\synopsis\<name>.synopsis.
|
||||
* Aliases (ls -> dir, rm -> del, ...) resolve to the
|
||||
* canonical command's file automatically.
|
||||
*
|
||||
* Caching (two layers)
|
||||
* --------------------
|
||||
* Parsing JSON and compiling a completion model on every TAB would be wasteful,
|
||||
* so results are cached:
|
||||
* 1. In memory, for the life of the shell session (command.js keeps the
|
||||
* require() handle, so this object persists across keystrokes).
|
||||
* 2. On disk, under \tvdos\cache\synopsis\, as a compiled-model blob. The
|
||||
* TSVM file layer exposes no reliable modification time, so the cache is
|
||||
* validated against the source file's *byte size* plus a CACHE_VERSION
|
||||
* stamp. A source edit that preserves the byte count will not invalidate
|
||||
* the disk cache -- an accepted trade-off. Every disk operation is
|
||||
* best-effort: a failure never breaks completion, it just falls back to
|
||||
* re-parsing.
|
||||
*
|
||||
* Public API
|
||||
* ----------
|
||||
* getCompletion(commandToken, prefixTokens, word) -> result | { ok:false }
|
||||
* getModel(commandToken) -> compiled model | null
|
||||
* getSummary(commandToken) -> one-line summary | null
|
||||
* getUsage(commandToken) -> generated usage string | null
|
||||
* resolveSynopsisPath(commandToken) -> full path | null
|
||||
* registerProvider(name, fn) -> register an `internal` completion source
|
||||
* clearCache() -> drop the in-memory caches
|
||||
*/
|
||||
|
||||
const TSF_VERSION = "1.0"
|
||||
const CACHE_VERSION = 1 // bump when compile()'s output shape changes
|
||||
const SYN_DIR = "\\tvdos\\synopsis" // built-in / coreutil synopses
|
||||
const CACHE_PARENT = "\\tvdos\\cache"
|
||||
const CACHE_DIR = "\\tvdos\\cache\\synopsis" // compiled-model disk cache
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// small local helpers (deliberately mirror command.js internals)
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function drive() { return (typeof _G !== "undefined" && _G.shell) ? _G.shell.getCurrentDrive() : "A" }
|
||||
|
||||
function trimStartRevSlash(s) {
|
||||
let cnt = 0
|
||||
while (cnt < s.length && s[cnt] === '\\') cnt += 1
|
||||
return s.substring(cnt)
|
||||
}
|
||||
|
||||
function isValidDriveLetter(l) {
|
||||
if (typeof l === 'string' || l instanceof String) {
|
||||
let lc = l.charCodeAt(0)
|
||||
return (l == '$' || 65 <= lc && lc <= 90 || 97 <= lc && lc <= 122)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function fileExists(p) { try { return files.open(p).exists } catch (e) { return false } }
|
||||
function fileSize(p) { try { return files.open(p).size | 0 } catch (e) { return 0 } }
|
||||
function readText(p) { try { let f = files.open(p); return f.exists ? f.sread() : null } catch (e) { return null } }
|
||||
|
||||
let _cacheDirReady = false
|
||||
function ensureCacheDir() {
|
||||
if (_cacheDirReady) return
|
||||
let d = drive()
|
||||
let segs = [CACHE_PARENT, CACHE_DIR]
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
try { let f = files.open(`${d}:${segs[i]}`); if (!f.exists) f.mkDir() } catch (e) { /* best-effort */ }
|
||||
}
|
||||
_cacheDirReady = true
|
||||
}
|
||||
function writeText(p, s) {
|
||||
try { ensureCacheDir(); files.open(p).swrite(s); return true } catch (e) { return false }
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// executable + synopsis-path resolution
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Find the runnable file a bare command name would resolve to, mirroring the
|
||||
// search order command.js uses (current directory, then PATH, with PATHEXT).
|
||||
function findExecutable(cmd) {
|
||||
let d = drive()
|
||||
if (isValidDriveLetter(cmd[0]) && cmd[1] === ':') {
|
||||
try { let f = files.open(cmd); return f.exists ? f.fullPath : null } catch (e) { return null }
|
||||
}
|
||||
let pwd = (typeof _G !== "undefined" && _G.shell) ? _G.shell.getPwd() : [""]
|
||||
let searchDir = (cmd.charAt(0) === '/') ? [""] : ["/" + pwd.join("/")].concat(_TVDOS.getPath())
|
||||
let pathExt = []
|
||||
if (cmd.split(".")[1] === undefined) {
|
||||
(_TVDOS.variables.PATHEXT || "").split(';').forEach(function (it) {
|
||||
if (it.length) { pathExt.push(it); pathExt.push(it.toUpperCase()) }
|
||||
})
|
||||
} else {
|
||||
pathExt.push("")
|
||||
}
|
||||
for (let i = 0; i < searchDir.length; i++) {
|
||||
for (let j = 0; j < pathExt.length; j++) {
|
||||
let search = searchDir[i]; if (!search.endsWith('\\')) search += '\\'
|
||||
let sp = trimStartRevSlash(search + cmd + pathExt[j])
|
||||
try { let f = files.open(`${d}:\\${sp}`); if (f.exists) return f.fullPath } catch (e) { /* keep looking */ }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Resolve a command token to the full path of its .synopsis document, or null.
|
||||
function resolveSynopsisPath(token) {
|
||||
if (!token) return null
|
||||
let d = drive()
|
||||
let lower = token.toLowerCase()
|
||||
|
||||
// built-in coreutil? -> \tvdos\synopsis\<name>.synopsis
|
||||
// try the typed name first, then any alias that shares the same function so
|
||||
// `ls` finds dir.synopsis without a duplicate file.
|
||||
if (typeof _G !== "undefined" && _G.shell && _G.shell.coreutils &&
|
||||
typeof _G.shell.coreutils[lower] === 'function') {
|
||||
let fn = _G.shell.coreutils[lower]
|
||||
let names = [lower]
|
||||
Object.keys(_G.shell.coreutils).forEach(function (k) {
|
||||
if (_G.shell.coreutils[k] === fn && names.indexOf(k) < 0) names.push(k)
|
||||
})
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
let p = `${d}:${SYN_DIR}\\${names[i]}.synopsis`
|
||||
if (fileExists(p)) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// app -> <executable>.synopsis colocated with the program
|
||||
let exe = findExecutable(token)
|
||||
if (!exe) return null
|
||||
let p = exe + ".synopsis"
|
||||
return fileExists(p) ? p : null
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// TSF compilation -- raw document -> completion model
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function compile(doc) {
|
||||
if (!doc || typeof doc !== 'object') return null
|
||||
let symbols = doc.symbols || {}
|
||||
|
||||
// ---- options: every symbol of kind "option" is an offerable flag ----
|
||||
let flags = [] // one entry per option symbol
|
||||
let flagMap = {} // flag string ("-r", "--recursive", "--no-recursive") -> entry
|
||||
Object.keys(symbols).forEach(function (id) {
|
||||
let s = symbols[id]
|
||||
if (!s || s.kind !== 'option') return
|
||||
let value = s.value || null
|
||||
let hasValue = !!value
|
||||
let entry = {
|
||||
id: id,
|
||||
long: s.long || null,
|
||||
short: s.short || null,
|
||||
summary: s.summary || '',
|
||||
negatable: !!s.negatable,
|
||||
hasValue: hasValue,
|
||||
valueRequired: hasValue ? (value.required !== false) : false,
|
||||
value: value
|
||||
}
|
||||
flags.push(entry)
|
||||
if (entry.long) flagMap[entry.long] = entry
|
||||
if (entry.short) flagMap[entry.short] = entry
|
||||
if (entry.negatable && entry.long) flagMap['--no-' + entry.long.replace(/^--/, '')] = entry
|
||||
})
|
||||
|
||||
// ---- positionals + subcommands, in grammar order ----
|
||||
let positionals = []
|
||||
let subcommands = []
|
||||
let seenSub = {}
|
||||
function walk(node, inRepeat) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
switch (node.type) {
|
||||
case 'sequence':
|
||||
case 'choice':
|
||||
(node.children || []).forEach(function (c) { walk(c, inRepeat) }); break
|
||||
case 'optional': walk(node.child, inRepeat); break
|
||||
case 'repeat': walk(node.child, true); break
|
||||
case 'oneOrMore': walk(node.child, true); break
|
||||
case 'reference': {
|
||||
let sym = symbols[node.symbol]
|
||||
if (!sym) return
|
||||
if (sym.kind === 'positional') {
|
||||
positionals.push({
|
||||
id: node.symbol,
|
||||
name: sym.name || node.symbol,
|
||||
type: sym.type || 'string',
|
||||
values: sym.values || null,
|
||||
completion: sym.completion || null,
|
||||
summary: sym.summary || '',
|
||||
repeatable: !!inRepeat
|
||||
})
|
||||
} else if (sym.kind === 'subcommand') {
|
||||
if (!seenSub[node.symbol]) {
|
||||
seenSub[node.symbol] = true
|
||||
subcommands.push({ name: sym.name || node.symbol, summary: sym.summary || '', tsf: sym.tsf || null })
|
||||
}
|
||||
}
|
||||
break // option / group references add no positional ordering
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
walk(doc.synopsis, false)
|
||||
|
||||
return {
|
||||
cacheVersion: CACHE_VERSION,
|
||||
tsfVersion: doc.tsfVersion || null,
|
||||
name: doc.name || null,
|
||||
summary: doc.summary || '',
|
||||
description: doc.description || '',
|
||||
symbols: symbols,
|
||||
synopsisNode: doc.synopsis || null,
|
||||
flags: flags,
|
||||
flagMap: flagMap,
|
||||
positionals: positionals,
|
||||
subcommands: subcommands,
|
||||
constraints: doc.constraints || []
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// loading + caching
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let _mem = {} // synopsisPath -> { srcSize, model }
|
||||
let _resolveMemo = {} // "drive|pwd|token" -> synopsisPath | null
|
||||
|
||||
function cacheKey(p) {
|
||||
// FNV-1a 32-bit hash, prefixed with a sanitised basename for readability.
|
||||
let h = 2166136261
|
||||
for (let i = 0; i < p.length; i++) { h ^= p.charCodeAt(i); h = (h * 16777619) >>> 0 }
|
||||
let base = (p.split(/[\\/]/).pop() || 'syn').replace(/[^A-Za-z0-9._-]/g, '_')
|
||||
return base + '_' + ('00000000' + h.toString(16)).slice(-8)
|
||||
}
|
||||
function cachePath(synPath) { return `${drive()}:${CACHE_DIR}\\${cacheKey(synPath)}.json` }
|
||||
|
||||
function loadModel(synPath) {
|
||||
if (!synPath) return null
|
||||
let srcSize = fileSize(synPath)
|
||||
|
||||
// 1. in-memory
|
||||
let mem = _mem[synPath]
|
||||
if (mem && mem.srcSize === srcSize) return mem.model
|
||||
|
||||
// 2. disk cache (size + version validated)
|
||||
let cachedText = readText(cachePath(synPath))
|
||||
if (cachedText) {
|
||||
try {
|
||||
let c = JSON.parse(cachedText)
|
||||
if (c && c.cacheVersion === CACHE_VERSION && c.srcSize === srcSize && c.model) {
|
||||
_mem[synPath] = { srcSize: srcSize, model: c.model }
|
||||
return c.model
|
||||
}
|
||||
} catch (e) { /* corrupt cache -> re-parse */ }
|
||||
}
|
||||
|
||||
// 3. parse the source
|
||||
let src = readText(synPath)
|
||||
if (src === null) return null
|
||||
let doc
|
||||
try { doc = JSON.parse(src) }
|
||||
catch (e) { try { serial.printerr("synopsis: bad JSON in " + synPath + ": " + e) } catch (_) {} ; return null }
|
||||
let model = compile(doc)
|
||||
if (!model) return null
|
||||
|
||||
_mem[synPath] = { srcSize: srcSize, model: model }
|
||||
writeText(cachePath(synPath), JSON.stringify({ cacheVersion: CACHE_VERSION, srcSize: srcSize, model: model }))
|
||||
return model
|
||||
}
|
||||
|
||||
function getModel(token) {
|
||||
if (!token) return null
|
||||
let key = drive() + '|' + ((typeof _G !== "undefined" && _G.shell) ? _G.shell.getPwdString() : '') + '|' + token
|
||||
let synPath
|
||||
if (Object.prototype.hasOwnProperty.call(_resolveMemo, key)) synPath = _resolveMemo[key]
|
||||
else { synPath = resolveSynopsisPath(token); _resolveMemo[key] = synPath }
|
||||
return synPath ? loadModel(synPath) : null
|
||||
}
|
||||
|
||||
function clearCache() { _mem = {}; _resolveMemo = {}; _cacheDirReady = false }
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// internal completion providers (for `"completion": { "method": "internal" }`)
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let _providers = {}
|
||||
function registerProvider(name, fn) { _providers[name] = fn }
|
||||
function safeProvider(name, word, model) {
|
||||
let fn = _providers[name]
|
||||
if (!fn) return []
|
||||
try { return fn(word, model) || [] } catch (e) { return [] }
|
||||
}
|
||||
|
||||
// "commands" -- runnable command names (coreutils + PATH executables).
|
||||
registerProvider('commands', function (word) {
|
||||
word = (word || '').toLowerCase()
|
||||
let out = [], seen = {}
|
||||
function add(n) { let k = n.toLowerCase(); if (seen[k]) return; seen[k] = true; out.push(n) }
|
||||
if (typeof _G !== "undefined" && _G.shell && _G.shell.coreutils)
|
||||
Object.keys(_G.shell.coreutils).forEach(function (k) { if (k.toLowerCase().indexOf(word) === 0) add(k) })
|
||||
try {
|
||||
let d = drive()
|
||||
let exts = (_TVDOS.variables.PATHEXT || "").split(';')
|
||||
.filter(function (e) { return e.length }).map(function (e) { return e.toLowerCase() })
|
||||
_TVDOS.getPath().forEach(function (dir) {
|
||||
let full = (dir === '') ? `${d}:\\` : `${d}:${dir.charAt(0) === '\\' ? dir : '\\' + dir}`
|
||||
try {
|
||||
let f = files.open(full); if (!f.exists || !f.isDirectory) return
|
||||
;(f.list() || []).forEach(function (it) {
|
||||
if (it.isDirectory) return
|
||||
let nl = (it.name || '').toLowerCase()
|
||||
if (!exts.some(function (e) { return nl.endsWith(e) })) return
|
||||
let nm = it.name
|
||||
exts.forEach(function (e) { if (nm.toLowerCase().endsWith(e)) nm = nm.substring(0, nm.length - e.length) })
|
||||
if (nm.toLowerCase().indexOf(word) === 0) add(nm)
|
||||
})
|
||||
} catch (e) { /* skip unreadable dir */ }
|
||||
})
|
||||
} catch (e) { /* ignore */ }
|
||||
return out
|
||||
})
|
||||
|
||||
// "envvars" -- environment variable names.
|
||||
registerProvider('envvars', function (word) {
|
||||
word = word || ''
|
||||
try {
|
||||
return Object.keys(_TVDOS.variables || {}).filter(function (k) {
|
||||
return k.toLowerCase().indexOf(word.toLowerCase()) === 0
|
||||
})
|
||||
} catch (e) { return [] }
|
||||
})
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// completion query
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Turn a `values` array (bare values or { value, summary } objects) into
|
||||
// completion candidates whose value matches `word` as a prefix.
|
||||
function valuesToCandidates(values, word) {
|
||||
if (!values) return []
|
||||
word = word || ''
|
||||
let out = []
|
||||
values.forEach(function (v) {
|
||||
let val, sum
|
||||
if (v && typeof v === 'object' && ('value' in v)) { val = '' + v.value; sum = v.summary || '' }
|
||||
else { val = '' + v; sum = '' }
|
||||
if (val.indexOf(word) === 0) out.push({ label: val, value: val + ' ', summary: sum, isDir: false })
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// Candidates implied by an argument descriptor (a positional, or an option's
|
||||
// `value`). Returns { candidates, filesystem } where `filesystem` is false or
|
||||
// one of 'path' | 'file' | 'directory' -- a request that the caller ALSO offer
|
||||
// matching filesystem entries.
|
||||
function descriptorCandidates(desc, word, model) {
|
||||
word = word || ''
|
||||
let none = { candidates: [], filesystem: false }
|
||||
if (!desc) return none
|
||||
|
||||
let method = (desc.completion && desc.completion.method) || (desc.type === 'enum' ? 'enum' : null)
|
||||
|
||||
// explicit completion block
|
||||
if (method === 'none') return none
|
||||
if (method === 'enum') return { candidates: valuesToCandidates(desc.values, word), filesystem: false }
|
||||
if (method === 'list') {
|
||||
let items = (desc.completion && (desc.completion.items || desc.completion.values)) || desc.values || []
|
||||
return { candidates: valuesToCandidates(items, word), filesystem: false }
|
||||
}
|
||||
if (method === 'internal') {
|
||||
let prov = desc.completion && desc.completion.provider
|
||||
return { candidates: valuesToCandidates(safeProvider(prov, word, model), word), filesystem: false }
|
||||
}
|
||||
// method 'command' (run a program for candidates) is intentionally not
|
||||
// executed here -- side-effect / latency safety -- so it falls through to
|
||||
// the type defaults below.
|
||||
|
||||
// no completion block (or unhandled method): default behaviour by type
|
||||
switch (desc.type) {
|
||||
case 'path': return { candidates: [], filesystem: 'path' }
|
||||
case 'file': return { candidates: [], filesystem: 'file' }
|
||||
case 'directory': return { candidates: [], filesystem: 'directory' }
|
||||
case 'boolean': return { candidates: valuesToCandidates(['true', 'false'], word), filesystem: false }
|
||||
case 'command': return { candidates: valuesToCandidates(safeProvider('commands', word, model), word), filesystem: false }
|
||||
case 'enum': return { candidates: valuesToCandidates(desc.values, word), filesystem: false }
|
||||
case 'user': if (_providers['users']) return { candidates: valuesToCandidates(safeProvider('users', word, model), word), filesystem: false }; break
|
||||
case 'group': if (_providers['groups']) return { candidates: valuesToCandidates(safeProvider('groups', word, model), word), filesystem: false }; break
|
||||
default: break
|
||||
}
|
||||
// string / integer / float / url / hostname / unknown: a soft `values`
|
||||
// list may still help; otherwise there is nothing to offer.
|
||||
if (desc.values) return { candidates: valuesToCandidates(desc.values, word), filesystem: false }
|
||||
return none
|
||||
}
|
||||
|
||||
// Every textual form a flag may be typed as (long, short, and the --no- form).
|
||||
function flagForms(entry) {
|
||||
let forms = []
|
||||
if (entry.long) forms.push(entry.long)
|
||||
if (entry.short) forms.push(entry.short)
|
||||
if (entry.negatable && entry.long) forms.push('--no-' + entry.long.replace(/^--/, ''))
|
||||
return forms
|
||||
}
|
||||
|
||||
// Count how many positional arguments `tokens` (the args already typed before
|
||||
// the caret) have consumed, skipping option flags and the values they take.
|
||||
function countPositionals(tokens, model) {
|
||||
let n = 0, skip = false
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let t = tokens[i]
|
||||
if (skip) { skip = false; continue } // this token was an option's value
|
||||
if (t.length > 0 && t.charAt(0) === '-') {
|
||||
if (t.indexOf('=') >= 0) continue // inline value -- no following value token
|
||||
let e = model.flagMap[t]
|
||||
if (e && e.hasValue && e.valueRequired) skip = true
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
function finalise(r) { return { ok: true, candidates: r.candidates, filesystem: r.filesystem } }
|
||||
|
||||
/*
|
||||
* Main entry point used by command.js.
|
||||
*
|
||||
* commandToken : the command (first word on the line)
|
||||
* prefixTokens : the argument tokens already typed, in order, EXCLUDING the
|
||||
* word currently under the caret
|
||||
* word : the partial word under the caret (may be "")
|
||||
*
|
||||
* Returns { ok:false } when there is no synopsis for the command (the caller
|
||||
* should fall back to its own default completion). Otherwise returns
|
||||
* { ok:true, candidates:[{label,value,summary,isDir}], filesystem:<flag> }
|
||||
* where `filesystem` (false | 'path' | 'file' | 'directory') asks the caller to
|
||||
* additionally offer matching filesystem entries.
|
||||
*/
|
||||
function getCompletion(commandToken, prefixTokens, word) {
|
||||
let model = getModel(commandToken)
|
||||
if (!model) return { ok: false }
|
||||
word = word || ''
|
||||
prefixTokens = prefixTokens || []
|
||||
|
||||
// (1) the caret is on an option flag
|
||||
if (word.length > 0 && word.charAt(0) === '-') {
|
||||
// inline value form: --flag=partial
|
||||
if (word.indexOf('--') === 0 && word.indexOf('=') >= 0) {
|
||||
let eq = word.indexOf('=')
|
||||
let flagPart = word.substring(0, eq)
|
||||
let valPart = word.substring(eq + 1)
|
||||
let entry = model.flagMap[flagPart]
|
||||
if (entry && entry.hasValue) {
|
||||
let r = descriptorCandidates(entry.value, valPart, model)
|
||||
r.candidates = r.candidates.map(function (c) {
|
||||
return { label: c.label, value: flagPart + '=' + c.value.replace(/ $/, '') + ' ', summary: c.summary, isDir: false }
|
||||
})
|
||||
return { ok: true, candidates: r.candidates, filesystem: false }
|
||||
}
|
||||
return { ok: true, candidates: [], filesystem: false }
|
||||
}
|
||||
// list flags matching the prefix
|
||||
let out = []
|
||||
model.flags.forEach(function (e) {
|
||||
flagForms(e).forEach(function (f) {
|
||||
if (f.indexOf(word) === 0) out.push({ label: f, value: f + ' ', summary: e.summary, isDir: false })
|
||||
})
|
||||
})
|
||||
return { ok: true, candidates: out, filesystem: false }
|
||||
}
|
||||
|
||||
// (2) the caret is on the value of the immediately preceding option
|
||||
let prev = prefixTokens.length > 0 ? prefixTokens[prefixTokens.length - 1] : null
|
||||
if (prev && prev.charAt(0) === '-' && prev.indexOf('=') < 0) {
|
||||
let entry = model.flagMap[prev]
|
||||
if (entry && entry.hasValue && entry.valueRequired)
|
||||
return finalise(descriptorCandidates(entry.value, word, model))
|
||||
}
|
||||
|
||||
// (3) a positional argument (or a subcommand in the first slot)
|
||||
let posIndex = countPositionals(prefixTokens, model)
|
||||
if (posIndex === 0 && model.subcommands.length > 0) {
|
||||
let out = model.subcommands
|
||||
.filter(function (s) { return s.name.indexOf(word) === 0 })
|
||||
.map(function (s) { return { label: s.name, value: s.name + ' ', summary: s.summary, isDir: false } })
|
||||
return { ok: true, candidates: out, filesystem: false }
|
||||
}
|
||||
let desc = null
|
||||
if (model.positionals.length > 0) {
|
||||
if (posIndex < model.positionals.length) desc = model.positionals[posIndex]
|
||||
else {
|
||||
let last = model.positionals[model.positionals.length - 1]
|
||||
if (last && last.repeatable) desc = last
|
||||
}
|
||||
}
|
||||
// No descriptor for this slot -> let the caller use its default completion.
|
||||
if (!desc) return { ok: false }
|
||||
return finalise(descriptorCandidates(desc, word, model))
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// generated help (per the spec, usage text is derived output, not normative)
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function grammarToText(node, symbols) {
|
||||
if (!node || typeof node !== 'object') return ''
|
||||
switch (node.type) {
|
||||
case 'sequence':
|
||||
return (node.children || []).map(function (c) { return grammarToText(c, symbols) })
|
||||
.filter(function (s) { return s.length }).join(' ')
|
||||
case 'choice':
|
||||
return '(' + (node.children || []).map(function (c) { return grammarToText(c, symbols) }).join(' | ') + ')'
|
||||
case 'optional':
|
||||
return '[' + grammarToText(node.child, symbols) + ']'
|
||||
case 'repeat': {
|
||||
// a repeat over a group is the familiar [OPTION...] slot
|
||||
let child = node.child
|
||||
if (child && child.type === 'reference' && symbols[child.symbol] && symbols[child.symbol].kind === 'group')
|
||||
return '[' + grammarToText(child, symbols) + '...]'
|
||||
return grammarToText(child, symbols) + '...'
|
||||
}
|
||||
case 'oneOrMore': {
|
||||
let t = grammarToText(node.child, symbols)
|
||||
return t + ' [' + t + '...]'
|
||||
}
|
||||
case 'reference': {
|
||||
let s = symbols[node.symbol]
|
||||
if (!s) return node.symbol
|
||||
if (s.kind === 'group') return 'OPTION'
|
||||
if (s.kind === 'option') return s.long || s.short || node.symbol
|
||||
if (s.kind === 'subcommand') return s.name || node.symbol
|
||||
if (s.kind === 'positional') return s.name || node.symbol
|
||||
return node.symbol
|
||||
}
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getUsage(token) {
|
||||
let m = getModel(token)
|
||||
if (!m) return null
|
||||
let body = grammarToText(m.synopsisNode, m.symbols)
|
||||
return ((m.name || token) + (body ? ' ' + body : '')).trim()
|
||||
}
|
||||
|
||||
function getSummary(token) {
|
||||
let m = getModel(token)
|
||||
return m ? (m.summary || '') : null
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Module exports
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
exports = {
|
||||
getCompletion,
|
||||
getModel,
|
||||
getSummary,
|
||||
getUsage,
|
||||
resolveSynopsisPath,
|
||||
registerProvider,
|
||||
clearCache,
|
||||
TSF_VERSION,
|
||||
}
|
||||
12
assets/disk0/tvdos/synopsis/cat.synopsis
Normal file
12
assets/disk0/tvdos/synopsis/cat.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "cat",
|
||||
"summary": "Print a file, or pipe its contents onward",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "File to read; reads from the pipe when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
}
|
||||
12
assets/disk0/tvdos/synopsis/cd.synopsis
Normal file
12
assets/disk0/tvdos/synopsis/cd.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "cd",
|
||||
"summary": "Change the current working directory",
|
||||
"symbols": {
|
||||
"dir": { "kind": "positional", "type": "directory", "name": "DIR", "summary": "Directory to change into; prints the current directory when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "dir" }
|
||||
}
|
||||
}
|
||||
22
assets/disk0/tvdos/synopsis/chvt.synopsis
Normal file
22
assets/disk0/tvdos/synopsis/chvt.synopsis
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "chvt",
|
||||
"summary": "Switch to virtual console N (1-6)",
|
||||
"symbols": {
|
||||
"console": {
|
||||
"kind": "positional",
|
||||
"type": "enum",
|
||||
"name": "N",
|
||||
"summary": "Target virtual console",
|
||||
"values": [
|
||||
{ "value": "1", "summary": "Virtual console 1" },
|
||||
{ "value": "2", "summary": "Virtual console 2" },
|
||||
{ "value": "3", "summary": "Virtual console 3" },
|
||||
{ "value": "4", "summary": "Virtual console 4" },
|
||||
{ "value": "5", "summary": "Virtual console 5" },
|
||||
{ "value": "6", "summary": "Virtual console 6" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "console" }
|
||||
}
|
||||
7
assets/disk0/tvdos/synopsis/cls.synopsis
Normal file
7
assets/disk0/tvdos/synopsis/cls.synopsis
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "cls",
|
||||
"summary": "Clear the screen",
|
||||
"symbols": {},
|
||||
"synopsis": { "type": "sequence", "children": [] }
|
||||
}
|
||||
16
assets/disk0/tvdos/synopsis/cp.synopsis
Normal file
16
assets/disk0/tvdos/synopsis/cp.synopsis
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "cp",
|
||||
"summary": "Copy a file",
|
||||
"symbols": {
|
||||
"source": { "kind": "positional", "type": "file", "name": "SOURCE", "summary": "File to copy from" },
|
||||
"dest": { "kind": "positional", "type": "path", "name": "DEST", "summary": "Destination path" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "source" },
|
||||
{ "type": "reference", "symbol": "dest" }
|
||||
]
|
||||
}
|
||||
}
|
||||
7
assets/disk0/tvdos/synopsis/date.synopsis
Normal file
7
assets/disk0/tvdos/synopsis/date.synopsis
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "date",
|
||||
"summary": "Print the system date and time",
|
||||
"symbols": {},
|
||||
"synopsis": { "type": "sequence", "children": [] }
|
||||
}
|
||||
9
assets/disk0/tvdos/synopsis/del.synopsis
Normal file
9
assets/disk0/tvdos/synopsis/del.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "del",
|
||||
"summary": "Delete a file",
|
||||
"symbols": {
|
||||
"file": { "kind": "positional", "type": "file", "name": "FILE", "summary": "File to delete" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "file" }
|
||||
}
|
||||
12
assets/disk0/tvdos/synopsis/dir.synopsis
Normal file
12
assets/disk0/tvdos/synopsis/dir.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "dir",
|
||||
"summary": "List the contents of a directory",
|
||||
"symbols": {
|
||||
"path": { "kind": "positional", "type": "directory", "name": "PATH", "summary": "Directory to list; the working directory when omitted" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "path" }
|
||||
}
|
||||
}
|
||||
12
assets/disk0/tvdos/synopsis/echo.synopsis
Normal file
12
assets/disk0/tvdos/synopsis/echo.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "echo",
|
||||
"summary": "Print text, expanding $VARIABLE references",
|
||||
"symbols": {
|
||||
"text": { "kind": "positional", "type": "string", "name": "TEXT", "summary": "Text to print", "completion": { "method": "none" } }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "repeat",
|
||||
"child": { "type": "reference", "symbol": "text" }
|
||||
}
|
||||
}
|
||||
7
assets/disk0/tvdos/synopsis/exit.synopsis
Normal file
7
assets/disk0/tvdos/synopsis/exit.synopsis
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "exit",
|
||||
"summary": "Exit the command processor",
|
||||
"symbols": {},
|
||||
"synopsis": { "type": "sequence", "children": [] }
|
||||
}
|
||||
9
assets/disk0/tvdos/synopsis/mkdir.synopsis
Normal file
9
assets/disk0/tvdos/synopsis/mkdir.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "mkdir",
|
||||
"summary": "Create a directory",
|
||||
"symbols": {
|
||||
"dir": { "kind": "positional", "type": "path", "name": "DIR", "summary": "Directory to create" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "dir" }
|
||||
}
|
||||
16
assets/disk0/tvdos/synopsis/mv.synopsis
Normal file
16
assets/disk0/tvdos/synopsis/mv.synopsis
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "mv",
|
||||
"summary": "Move or rename a file",
|
||||
"symbols": {
|
||||
"source": { "kind": "positional", "type": "file", "name": "SOURCE", "summary": "File to move" },
|
||||
"dest": { "kind": "positional", "type": "path", "name": "DEST", "summary": "Destination path" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "reference", "symbol": "source" },
|
||||
{ "type": "reference", "symbol": "dest" }
|
||||
]
|
||||
}
|
||||
}
|
||||
7
assets/disk0/tvdos/synopsis/panic.synopsis
Normal file
7
assets/disk0/tvdos/synopsis/panic.synopsis
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "panic",
|
||||
"summary": "Deliberately raise an error (diagnostic aid)",
|
||||
"symbols": {},
|
||||
"synopsis": { "type": "sequence", "children": [] }
|
||||
}
|
||||
12
assets/disk0/tvdos/synopsis/rem.synopsis
Normal file
12
assets/disk0/tvdos/synopsis/rem.synopsis
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "rem",
|
||||
"summary": "A comment; the line is ignored",
|
||||
"symbols": {
|
||||
"text": { "kind": "positional", "type": "string", "name": "TEXT", "summary": "Comment text", "completion": { "method": "none" } }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "repeat",
|
||||
"child": { "type": "reference", "symbol": "text" }
|
||||
}
|
||||
}
|
||||
18
assets/disk0/tvdos/synopsis/set.synopsis
Normal file
18
assets/disk0/tvdos/synopsis/set.synopsis
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "set",
|
||||
"summary": "Set or display an environment variable",
|
||||
"symbols": {
|
||||
"assignment": {
|
||||
"kind": "positional",
|
||||
"type": "string",
|
||||
"name": "NAME=VALUE",
|
||||
"summary": "Variable assignment, or a name to display; lists all variables when omitted",
|
||||
"completion": { "method": "internal", "provider": "envvars" }
|
||||
}
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "optional",
|
||||
"child": { "type": "reference", "symbol": "assignment" }
|
||||
}
|
||||
}
|
||||
7
assets/disk0/tvdos/synopsis/ver.synopsis
Normal file
7
assets/disk0/tvdos/synopsis/ver.synopsis
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "ver",
|
||||
"summary": "Print the operating system version",
|
||||
"symbols": {},
|
||||
"synopsis": { "type": "sequence", "children": [] }
|
||||
}
|
||||
9
assets/disk0/tvdos/synopsis/which.synopsis
Normal file
9
assets/disk0/tvdos/synopsis/which.synopsis
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "which",
|
||||
"summary": "Report how a command name resolves",
|
||||
"symbols": {
|
||||
"program": { "kind": "positional", "type": "command", "name": "PROGRAM", "summary": "Command name to resolve" }
|
||||
},
|
||||
"synopsis": { "type": "reference", "symbol": "program" }
|
||||
}
|
||||
BIN
assets/disk0/tvdos/tsvm.chr
Normal file
BIN
assets/disk0/tvdos/tsvm.chr
Normal file
Binary file not shown.
@@ -1,5 +1,7 @@
|
||||
\begin{itemlist}
|
||||
\item Song, Minjae. 2021. ``Terran BASIC Reference Manual for Language Version 1.2, Third Edition''.
|
||||
\item Bradner, S. 1997. ``Key words for use in RFCs to Indicate Requirement Levels.'' RFC 2119. \url{https://www.rfc-editor.org/rfc/rfc2119}.
|
||||
\item Leiba, B. 2017. ``Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words.'' RFC 8174. \url{https://www.rfc-editor.org/rfc/rfc8174}.
|
||||
\item Wikipedia. ``List of DOS commands.'' Updated 2022-08-29 15:00. \url{https://en.wikipedia.org/wiki/List_of_DOS_commands}.
|
||||
\item Wikipedia. ``Pipeline (software).'' Updated 2022-07-17 06:21. \url{https://en.wikipedia.org/wiki/Pipeline_(software)}.
|
||||
\end{itemlist}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
\section{Specs}
|
||||
|
||||
\begin{outline}
|
||||
\1 16 MB memory space with maximum 8 MB of scratchpad memory
|
||||
\1 16 MB memory space with maximum 8 MB of core memory
|
||||
\1 7 peripheral card slots, each can map 1 MB of memory to the memory space
|
||||
\1 Standard graphics adapter on slot 1, with 256 simultaneous colours, 560\times448 pixels framebuffer and 80-column 32-row text buffer
|
||||
\1 Built-in mouse input support
|
||||
\1 4 serial ports to connect disk drives, modems and other computers
|
||||
\end{outline}
|
||||
|
||||
There are three memories on the system: Hardware Memory (8 MB), Scratchpad Memory (up to 8 MB) and Program Memory (infinite!)
|
||||
There are three memories on the system: Hardware Memory (8 MB), Core Memory (up to 8 MB) and Program Memory (infinite!)
|
||||
|
||||
Your Javascript program is stored into the Program Memory, and since its capacity is limitless, you can put a large graphics directly into your Javascript source code, but the Program Memory is the slowest of all three memories. For faster graphics, you need to store them onto the Scratchpad Memory then DMA-Copy them to the graphics adapter.
|
||||
Your Javascript program is stored into the Program Memory, and since its capacity is limitless, you can put a large graphics directly into your Javascript source code, but the Program Memory is the slowest of all three memories. For faster graphics, you need to store them onto the Core Memory then DMA-Copy them to the graphics adapter.
|
||||
|
||||
\section{Javascript Extensions}
|
||||
|
||||
@@ -36,7 +36,29 @@ Your Javascript program is stored into the Program Memory, and since its capacit
|
||||
|
||||
\chapter{Libraries}
|
||||
|
||||
\thismachine\ runs your program on an embedded ECMAScript engine. The language is standard Javascript; what makes a \thismachine\ program a \thismachine\ program is the set of \emph{host namespaces} the engine exposes --- global objects, available without any import, through which the program talks to the hardware.
|
||||
|
||||
\section{Namespaces}
|
||||
|
||||
\index{namespaces}The following namespaces are always present, whether or not an operating system is loaded:
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{sys}{low-level system and memory access (\code{peek}, \code{poke}, \code{malloc}, timing, input).}
|
||||
\1\inlinesynopsis{graphics}{the graphics adapter: pixels, palette, graphics modes, image decoding.}
|
||||
\1\inlinesynopsis{audio}{the sound card: PCM playback and the tracker engine.}
|
||||
\1\inlinesynopsis{com}{block (``serial'') communication with attached devices.}
|
||||
\1\inlinesynopsis{dma}{bulk memory-to-memory and memory-to-device transfers.}
|
||||
\1\inlinesynopsis{gzip}{compression and decompression (see the note on the name below).}
|
||||
\1\inlinesynopsis{base64}{Base64 encoding and decoding.}
|
||||
\1\inlinesynopsis{serial}{a debug text channel to the host; output appears on the host's console, not on the \thismachine\ screen.}
|
||||
\1\inlinesynopsis{con}{screen text manipulation, built on top of the above.}
|
||||
\end{outline}
|
||||
|
||||
The plain \code{print}, \code{println}, \code{printerr}, \code{printerrln} and \code{read} functions are also global.
|
||||
|
||||
A second set of namespaces --- \code{files}, \code{input}, \code{unicode}, \code{GL}, \code{require}, \code{exports} and the \code{\_G.shell} family --- is provided by \thedos\ and is only present once the operating system has booted. Those are documented in the \thedos\ part of this book.
|
||||
|
||||
\textbf{A note on \code{gzip}:} despite the name, the \code{gzip} namespace compresses and decompresses using the \emph{Zstandard} format. The decompressor recognises both Zstandard and the older gzip streams, so older files keep working. The name is historical.
|
||||
|
||||
\section{Standard Input and Output}
|
||||
|
||||
@@ -96,6 +118,8 @@ Functions:
|
||||
\1\formalsynopsis{color\_fore}{code: Int}{Defines the foreground colour of the text. 0 -- black, 1 -- red, 2 -- green, 3 -- yellow, 4 -- blue, 5 -- magenta, 6 -- cyan, 7 -- white, -1 -- transparent}
|
||||
\1\formalsynopsis{color\_back}{code: Int}{Defines the background colour of the text.}
|
||||
\1\formalsynopsis{color\_pair}{fore: Int, back: Int}{Defines the foreground and background colour of the text. Colour code for this function differs from the \code{color\_back} and \code{color\_fore}; please refer to the \ref{colourpalette}.}
|
||||
\1\formalsynopsis{get\_color\_fore}{}[Int]{Returns the current text foreground colour (palette index).}
|
||||
\1\formalsynopsis{get\_color\_back}{}[Int]{Returns the current text background colour (palette index).}
|
||||
\1\formalsynopsis{clear}{}{Clears the text buffer. The framebuffer (if any) will not be affected.}
|
||||
\1\formalsynopsis{reset\_graphics}{}{Resets foreground and background colour to defaults and makes the cursor visible if it was hidden.}
|
||||
\end{outline}
|
||||
@@ -146,7 +170,7 @@ Sys library allows programmers to manipulate the system in low-level.
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{poke}{address: Int, value: Int}{Puts a value into the memory of the specified address.}
|
||||
\1\formalsynopsis{peek}{address: Int}[Int]{Reads a value from the memory of the specified address.}
|
||||
\1\formalsynopsis{malloc}{size: Int}[Int]{Allocates a space of the given size on the Scratchpad memory and returns its pointer (starting address)}
|
||||
\1\formalsynopsis{malloc}{size: Int}[Int]{Allocates a space of the given size on the Core memory and returns its pointer (starting address)}
|
||||
\1\formalsynopsis{free}{pointer: Int}{Frees the memory space previously \code{malloc}'d}
|
||||
\1\formalsynopsis{memcpy}{from: Int, to: Int, length: Int}{Copies the memory block of the given length. From and To are pointers.}
|
||||
\1\formalsynopsis{mapRom}{romSlotNum: Int}{Maps the contents on the given ROM to the memory address {-\nobreak65537.\nobreak.-\nobreak131072}}
|
||||
@@ -163,7 +187,7 @@ Sys library allows programmers to manipulate the system in low-level.
|
||||
\1\formalsynopsis{waitForMemChg}{address: Int, andMask: Int, xorMask: Int}[]{Do nothing until a memory value is changed. More specifically, this function will \code{spin} while:$$ (\mathrm{peek}(addr)\ \mathrm{xor}\ xorMask)\ \mathrm{and}\ andMask = 0 $$}
|
||||
\1\formalsynopsis{getSysRq}{}[Boolean]{Returns true if System Request key is down.}
|
||||
\1\formalsynopsis{unsetSysRq}{}{After using the System Request key, call this function to `release' the key so that other programs can use it.}
|
||||
\1\formalsynopsis{maxmem}{}[Int]{returns the size of the Scratchpad Memory in bytes.}
|
||||
\1\formalsynopsis{maxmem}{}[Int]{returns the size of the Core Memory in bytes.}
|
||||
\1\formalsynopsis{getUsedMem}{}[Int]{Returns how many memories can be \code{malloc}'d.}
|
||||
\1\formalsynopsis{getMallocStatus}{}[IntArray(2)]{Returns the \code{malloc} status in following order:$$ [\mathrm{Malloc\ unit\ size,\ allocated\ block\ counts}] $$}
|
||||
\end{outline}
|
||||
@@ -172,6 +196,37 @@ Sys library allows programmers to manipulate the system in low-level.
|
||||
|
||||
|
||||
|
||||
\section{DMA}
|
||||
|
||||
\index{dma (library)}The \thismachine\ can copy blocks of memory directly, without the program shuffling individual bytes. This is far faster than a \code{peek}/\code{poke} loop and is the usual way of moving graphics into the framebuffer.
|
||||
|
||||
\namespaceis{DMA}{dma}
|
||||
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{ramToRam}{from: Int, to: Int, length: Int}{Copies a block of memory from one address to another.}
|
||||
\1\formalsynopsis{ramToFrame}{from: Int, to: Int, length: Int}{Copies a block of memory into the framebuffer of the first graphics adapter.}
|
||||
\1\formalsynopsis{ramToFrame}{from: Int, devnum: Int, offset: Int, length: Int}{Copies a block of memory into the framebuffer of the graphics adapter in the given slot, starting at the given offset.}
|
||||
\1\formalsynopsis{frameToRam}{from: Int, to: Int, length: Int}{Copies a block of framebuffer memory back into ordinary memory.}
|
||||
\1\formalsynopsis{frameToRam}{from: Int, to: Int, devnum: Int, length: Int}{As above, sourcing the framebuffer of the graphics adapter in the given slot.}
|
||||
\1\formalsynopsis{comToRam}{portNo: Int, srcOff: Int, destOff: Int, length: Int}{Copies bytes from a serial port's receive block into memory.}
|
||||
\1\formalsynopsis{ramToCom}{srcOff: Int, portNo: Int, length: Int}{Copies bytes from memory into a serial port's send block.}
|
||||
\1\formalsynopsis{strToRam}{str: String, to: Int, srcOff: Int, length: Int}{Writes the characters of a Javascript string into memory as bytes.}
|
||||
\end{outline}
|
||||
|
||||
|
||||
\section{Serial Debugger}
|
||||
|
||||
\index{serial (library)}The \code{serial} namespace prints to a debug channel that appears on the \emph{host} machine's console, never on the \thismachine\ screen. It is invaluable for tracing a program without disturbing what the user sees.
|
||||
|
||||
\namespaceis{Serial}{serial}
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{print}[Anything]{prints a value to the host debug console.}
|
||||
\1\inlinesynopsis{println}[Anything]{prints a value followed by a new line to the host debug console.}
|
||||
\end{outline}
|
||||
|
||||
|
||||
|
||||
\chapter{Serial Communication}
|
||||
|
||||
Some peripherals such as disk drives are connected through the ``Serial Communication''.
|
||||
@@ -328,7 +383,13 @@ The com-port will behave differently if you're writing to or reading from the ad
|
||||
|
||||
\section{Keyboard}
|
||||
|
||||
TODO
|
||||
\index{keyboard}The keyboard is part of the main hardware (the slot-zero IO device) and is read through MMIO. It offers two distinct views of the same physical keyboard, and a program chooses whichever suits it.
|
||||
|
||||
\textbf{The input stream (TTY view).} For ordinary line- and character-oriented input, the program opens the text input stream by writing a non-zero value to MMIO \code{-39}. Keystrokes are then translated to characters and pushed onto the \emph{keyboard input buffer}; the head of that buffer is read from MMIO \code{-38}, and reading it shifts the buffer along. This is the mechanism behind \code{con.getch}, \code{read} and the \code{sys.read}/\code{sys.readKey} family. Opening the stream clears the buffer, so it must be opened exactly once.
|
||||
|
||||
\textbf{The raw view.} For interactive programs that need to know which keys are physically held down --- games, editors --- the program latches the current input by writing to MMIO \code{-40} and then reads the \emph{list of pressed keys} from \code{-41..-48}. This buffer holds up to eight simultaneous keys (``8-key rollover''), sorted, with zero filling the unused slots. \code{con.poll\_keys} exposes this directly. Latching freezes the values so that a multi-key read is consistent.
|
||||
|
||||
The codes in the raw view are the engine keycodes listed below; the codes in the stream view are character codes (with a handful of control values, e.g.\ \code{3} for Ctrl-C, \code{8} for Backspace, \code{13} for Return, and \code{19}--\code{22} for the arrow keys). \thedos\ builds its event-driven \code{input} library (see the \thedos\ part) on top of the raw view.
|
||||
|
||||
\subsection{Keycodes}
|
||||
|
||||
@@ -451,7 +512,14 @@ F12 & 142 \\
|
||||
|
||||
\section{Mouse}
|
||||
|
||||
TODO
|
||||
\index{mouse}The mouse is read through the same slot-zero IO device. Its position and button state are memory-mapped:
|
||||
|
||||
\begin{outline}
|
||||
\1the cursor's X position is a 16-bit value at MMIO \code{-33..-34}, and the Y position at \code{-35..-36}. Like the keyboard's raw view, both are latched by a write to \code{-40} so a coordinate pair can be read consistently.
|
||||
\1the button state is a single byte at \code{-37}. It is a bit-field: bit 0 is the left button, bit 1 the right, bit 2 the middle. Bits 6 and 7 report a wheel notch up and down respectively; these wheel bits latch in hardware and clear when read, so a single notch is reported exactly once.
|
||||
\end{outline}
|
||||
|
||||
Most programs do not poke these addresses directly. Under \thedos, the \code{input} library turns mouse motion, clicks and wheel notches into the \textbf{mouse\_down}, \textbf{mouse\_up}, \textbf{mouse\_move} and \textbf{mouse\_wheel} events described in the \thedos\ part of this book.
|
||||
|
||||
|
||||
\chapter{Peripherals and Memory Mapping}
|
||||
@@ -490,7 +558,7 @@ The memory map of \thismachine\ is illustrated as following:
|
||||
\draw(0,12.5) node[anchor=north east] {Start};
|
||||
\draw(6,12.5) node[anchor=north west] {End};
|
||||
|
||||
\draw(3,10) node[anchor=mid] {\memlabel{Scratchpad Memory}};
|
||||
\draw(3,10) node[anchor=mid] {\memlabel{Core Memory}};
|
||||
\draw(3,7.5) node[anchor=mid] {\memlabel{MMIO Area}};
|
||||
\draw(3,6.5) node[anchor=mid] {\memlabel{Peripheral Memory \#1}};
|
||||
\draw(3,5.5) node[anchor=mid] {\memlabel{Peripheral Memory \#2}};
|
||||
@@ -559,7 +627,7 @@ Address & RW & Description \\
|
||||
-41..-48 & RO & List of pressed keys (latched by -40) \\
|
||||
-49 & RO & System Flags A (\code{0b r000 000t}, where r: RESET button held, t: STOP button held) \\
|
||||
-50..-52 & RO & Unused System Flags \\
|
||||
-65..-68 & RO & Size of the Scratchpad Memory \\
|
||||
-65..-68 & RO & Size of the Core Memory \\
|
||||
-69 & WO & Counter Latch (\code{0b01}--Uptime, \code{0b10}--RTC) \\
|
||||
-73..-80 & RO & System Uptime in nanoseconds (latched by -69) \\
|
||||
-81..-88 & RO & RTC in nanoseconds (latched by -69) \\
|
||||
@@ -578,7 +646,14 @@ Address & RW & Description \\
|
||||
|
||||
\chapter{Text and Graphics Display}
|
||||
|
||||
TODO: Textbuf, pixelbuf, graphics mode, chart of the draw order
|
||||
\index{display model}The reference graphics adapter drives the screen from two superimposed surfaces:
|
||||
|
||||
\begin{outline}
|
||||
\1the \textbf{framebuffer} (also called the pixel buffer), a grid of pixels whose size and colour depth depend on the current graphics mode; and
|
||||
\1the \textbf{text buffer}, a grid of character cells, each with its own foreground and background colour.
|
||||
\end{outline}
|
||||
|
||||
These are composited every frame in a fixed order: first the screen background (border) colour, then the framebuffer, then the text on top. The text is therefore always visible over whatever the framebuffer holds, and a text cell whose background uses the transparent palette entry lets the framebuffer show through. A program that wants a purely graphical display simply leaves the text buffer blank; one that wants a classic terminal leaves the framebuffer cleared.
|
||||
|
||||
While the graphics adapters can be plugged into any peripheral slot, it is highly recommended they occupy the 1st slot. The Memory address charts for the graphics adapter on this documentation will assume as such.
|
||||
|
||||
@@ -634,7 +709,35 @@ Sequence & Description \\
|
||||
|
||||
\section{Frame Buffer}
|
||||
|
||||
TODO
|
||||
\index{frame buffer}The framebuffer holds the pixel image. In the default mode it is $560\times448$ pixels, one byte per pixel, where each byte is an index into the 256-entry colour palette. Pixels are written either with the \code{graphics} library (\code{plotPixel}, \code{plotRect}, \dots), or far more quickly by \code{dma}-copying a prepared image into the framebuffer region of the adapter's memory.
|
||||
|
||||
\subsection{Graphics Modes}
|
||||
|
||||
\index{graphics mode}The adapter can be reconfigured into several modes that trade resolution, colour depth and the number of \emph{layers} (independent pixel planes that the adapter composites together). The mode is selected with \code{graphics.setGraphicsMode}, or by poking the Current Graphics Mode register. The higher modes need extra video-memory banks to be fitted; if the required banks are absent, the mode change is ignored.
|
||||
|
||||
\begin{center}
|
||||
\begin{tabulary}{\textwidth}{clcl}
|
||||
Mode & Resolution & Colours & Layers / requirement \\
|
||||
\hline
|
||||
0 & $560\times448$ & 256 & 1 layer (default) \\
|
||||
1 & $280\times224$ & 256 & 4 layers \\
|
||||
2 & $280\times224$ & 4096 & 2 layers \\
|
||||
3 & $560\times448$ & 256 & 2 layers (needs bank 2) \\
|
||||
4 & $560\times448$ & 4096 & 1 layer (needs bank 2) \\
|
||||
5 & $560\times448$ & 15-bit & 1 layer (needs bank 2) \\
|
||||
8 & $560\times448$ & 24-bit & 1 layer (needs banks 3 \& 4) \\
|
||||
\end{tabulary}
|
||||
\end{center}
|
||||
|
||||
A graphics adapter ships with one 256\,kB memory bank and can hold up to four. The number of installed banks is reported by the adapter and limits which modes are available.
|
||||
|
||||
\subsection{Direct Colour}
|
||||
|
||||
\index{direct colour}The 4096-colour (``direct colour'') modes do not use the palette. Instead two layers are paired to form one frame: the low layer carries the red and green channels (\code{0b RRRR GGGG}) and the high layer carries the blue channel and a 4-bit transparency (\code{0b BBBB AAAA}), giving 4096 colours each with 16 levels of transparency.
|
||||
|
||||
\subsection{Layers and Scrolling}
|
||||
|
||||
\index{layers}When a mode provides more than one layer, the \emph{layer arrangement} register chooses the order in which the layers are stacked, so a program can swap front and back planes without moving any pixels. The whole framebuffer can also be panned: the horizontal and vertical framebuffer-scroll registers shift the entire image, and a per-scanline horizontal offset table allows each scanline to be displaced independently --- the basis of split-screen and wavy ``raster'' effects.
|
||||
|
||||
\subsection{Colour Palette}
|
||||
\label{colourpalette}
|
||||
@@ -940,14 +1043,55 @@ TODO
|
||||
|
||||
\section{The Graphics Library}
|
||||
|
||||
\index{graphics (library)}Graphics library provides basic functions to communicate and manipulate the graphics adapter.
|
||||
\index{graphics (library)}Graphics library provides basic functions to communicate and manipulate the graphics adapter. Coordinates are in pixels with the origin at the top-left; colours are palette indices unless the adapter is in a direct-colour mode.
|
||||
|
||||
\namespaceis{Graphics}{graphics}
|
||||
|
||||
Drawing:
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{TODO}{to be added.}
|
||||
\1\formalsynopsis{plotPixel}{x: Int, y: Int, colour: Int}{Plots a single pixel on the first framebuffer.}
|
||||
\1\formalsynopsis{plotPixel2}{x: Int, y: Int, colour: Int}{Plots a single pixel on the second framebuffer.}
|
||||
\1\formalsynopsis{plotRect}{x: Int, y: Int, w: Int, h: Int, colour: Int}{Fills a rectangle on the first framebuffer. An optional sixth argument selects a blending effect.}
|
||||
\1\formalsynopsis{plotRect2}{x: Int, y: Int, w: Int, h: Int, colour: Int}{As \code{plotRect}, on the second framebuffer.}
|
||||
\1\formalsynopsis{clearPixels}{col: Int}{Fills the entire framebuffer with the given colour.}
|
||||
\1\formalsynopsis{clearText}{}{Blanks the text buffer.}
|
||||
\1\formalsynopsis{getGpuMemBase}{}[Int]{Returns the base address of the graphics adapter's memory, for direct access via \code{peek}/\code{poke} or \code{dma}.}
|
||||
\end{outline}
|
||||
|
||||
Colour:
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{setBackground}{r: Int, g: Int, b: Int}{Sets the screen background (border) colour, 8 bits per channel.}
|
||||
\1\formalsynopsis{resetPalette}{}{Restores the default colour palette.}
|
||||
\1\formalsynopsis{setPalette}{index: Int, r: Int, g: Int, b: Int, a: Int}{Sets one palette entry. Channels are 4-bit (0--15); \code{a} (alpha) defaults to 15 (opaque).}
|
||||
\1\formalsynopsis{setTextFore}{b: Int}{Sets the current text foreground colour.}
|
||||
\1\formalsynopsis{setTextBack}{b: Int}{Sets the current text background colour.}
|
||||
\1\formalsynopsis{getTextFore}{}[Int]{Returns the current text foreground colour.}
|
||||
\1\formalsynopsis{getTextBack}{}[Int]{Returns the current text background colour.}
|
||||
\end{outline}
|
||||
|
||||
Mode and geometry:
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{setGraphicsMode}{mode: Int}{Switches the graphics mode (see \emph{Graphics Modes}).}
|
||||
\1\formalsynopsis{getGraphicsMode}{}[Int]{Returns the current graphics mode.}
|
||||
\1\formalsynopsis{getPixelDimension}{}[IntArray(2)]{Returns the framebuffer size as \code{[width, height]}.}
|
||||
\1\formalsynopsis{getTermDimension}{}[IntArray(2)]{Returns the text grid size as \code{[columns, rows]}.}
|
||||
\1\formalsynopsis{setFramebufferScroll}{x: Int, y: Int}{Pans the whole framebuffer to the given offset.}
|
||||
\1\formalsynopsis{getFramebufferScroll}{}[IntArray(2)]{Returns the current framebuffer scroll offset.}
|
||||
\1\formalsynopsis{scrollFrame}{xdelta: Int, ydelta: Int}{Pans the framebuffer by a relative amount.}
|
||||
\1\formalsynopsis{setLineOffset}{line: Int, offset: Int}{Sets the per-scanline horizontal offset (raster effects).}
|
||||
\1\formalsynopsis{getLineOffset}{line: Int}[Int]{Returns the per-scanline horizontal offset.}
|
||||
\end{outline}
|
||||
|
||||
Text cursor and symbols (these talk to the adapter directly; under \thedos\ prefer the \code{con} library):
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{getCursorYX}{}[IntArray(2)]{Returns the text cursor position as \code{[row, column]}.}
|
||||
\1\formalsynopsis{setCursorYX}{cy: Int, cx: Int}{Moves the text cursor.}
|
||||
\1\formalsynopsis{putSymbol}{c: Int}{Writes the character of the given code at the cursor, without advancing it.}
|
||||
\1\formalsynopsis{putSymbolAt}{cy: Int, cx: Int, c: Int}{Writes a character at a specific cell.}
|
||||
\end{outline}
|
||||
|
||||
Image decoding: the adapter can decode compressed still images straight into memory. \code{decodeImage(srcFilePtr, srcFileLen)} decodes an image (JPEG, PNG, TGA, \dots) whose bytes are already in memory and returns its dimensions; \code{decodeImageTo} writes the result to a given buffer, and \code{decodeImageResample} scales it on the way. These, together with the hardware decoders for the iPF, TEV and TAV formats, are what the \thedos\ media players are built on; the formats themselves are described in the \thedos\ part.
|
||||
|
||||
|
||||
\section{MMIO and Memory Mapping}
|
||||
|
||||
@@ -987,3 +1131,43 @@ Address & RW & Description \\
|
||||
-1310209..-1310720 & RW & Palettes in This Pattern: {\ttfamily 0b RRRR GGGG; 0b BBBB AAAA} \\
|
||||
-1310721..-1561600 & RW & Second Framebuffer \\
|
||||
\end{tabulary}
|
||||
|
||||
|
||||
|
||||
\chapter{Audio}
|
||||
|
||||
\index{audio adapter}The \thismachine\ sound card is built from four independent \textbf{playheads}. Each playhead can play either a stream of PCM samples or a tracker track, and the four are mixed together to the card's native output of 32\,kHz stereo.
|
||||
|
||||
The playheads are not locked to one another, so timing between them is not guaranteed. A single piece of music should therefore live on a single playhead; the remaining playheads are best used for sound effects or left idle.
|
||||
|
||||
\section{PCM Playback}
|
||||
|
||||
\index{PCM playback}In PCM mode a playhead is fed a queue of raw samples, which the card plays back at the hardware rate. This is the path used by the \thedos\ players for raw PCM, WAV/ADPCM, MP2 and \thedos's own compressed audio --- a player decodes the file into samples and pushes them into a playhead's queue. The relevant format details are covered, from the listener's point of view, in the \thedos\ part.
|
||||
|
||||
\section{Tracker Playback}
|
||||
|
||||
\index{tracker engine}In tracker mode a playhead is driven by the card's built-in tracker engine. The engine keeps a pool of digitised \emph{samples}, a set of \emph{instruments} built from them, and the \emph{patterns} and cue sheet that sequence the notes; once these are loaded the playhead renders the song autonomously. This is how \thismachine\ plays its native \textbf{Taud} music.
|
||||
|
||||
This guide deliberately stops at that overview. The Taud file format, the instrument and envelope model, the effect commands, and the \emph{Microtone} tracker used to author Taud music are large topics with a manual of their own, and are out of scope here.
|
||||
|
||||
\section{The Audio Library}
|
||||
|
||||
\index{audio (library)}The \code{audio} namespace controls the playheads. The functions below cover everyday playback; the engine also exposes a large number of tracker-control and voice-inspection calls (pattern and cue upload, tempo, per-voice queries, \dots) that belong with the Taud documentation.
|
||||
|
||||
\namespaceis{Audio}{audio}
|
||||
|
||||
\begin{outline}
|
||||
\1\formalsynopsis{setPcmMode}{playhead: Int}{Puts a playhead (0--3) into PCM mode.}
|
||||
\1\formalsynopsis{setTrackerMode}{playhead: Int}{Puts a playhead into tracker mode.}
|
||||
\1\formalsynopsis{play}{playhead: Int}{Starts the playhead.}
|
||||
\1\formalsynopsis{stop}{playhead: Int}{Stops the playhead.}
|
||||
\1\formalsynopsis{isPlaying}{playhead: Int}[Boolean]{Whether the playhead is currently playing.}
|
||||
\1\formalsynopsis{getPosition}{playhead: Int}[Int]{Current playback position of the playhead.}
|
||||
\1\formalsynopsis{setMasterVolume}{playhead: Int, volume: Int}{Sets the playhead's output volume.}
|
||||
\1\formalsynopsis{setMasterPan}{playhead: Int, pan: Int}{Sets the playhead's stereo pan.}
|
||||
\1\formalsynopsis{putPcmDataByPtr}{playhead: Int, ptr: Int, length: Int, destOffset: Int}{Copies PCM samples from memory into the playhead's buffer.}
|
||||
\1\formalsynopsis{purgeQueue}{playhead: Int}{Empties the playhead's pending sample queue.}
|
||||
\1\formalsynopsis{resetParams}{playhead: Int}{Resets the playhead to default parameters.}
|
||||
\1\formalsynopsis{getMemAddr}{}[Int]{Returns the base address of the audio adapter's memory space.}
|
||||
\1\formalsynopsis{getBaseAddr}{}[Int]{Returns the base address of the audio adapter's MMIO area.}
|
||||
\end{outline}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
\thismachine\ is a virtual machine programmable using mainly, but not limited to, Javascript, and can have 7 virtual peripherals that can be communicated using MMIOs exclusively. \thismachine\ has default graphics of 80-column 32-rows text mode with one 560\times448 pixels framebuffer with 256 palette colours with 4096 colours to choose from.
|
||||
\thismachine\ is a virtual machine that imitates the architecture of an 8-bit era home computer while being programmed, mainly but not exclusively, in Javascript. A \thismachine\ system is built around a flat memory space into which both the core memory and the hardware peripherals are mapped, so that every device --- the graphics adapter, the sound card, the disk drives --- is reached through the same \code{peek} and \code{poke} operations that touch ordinary memory.
|
||||
|
||||
This is the documentation for \thismachine\ \tsvmver.
|
||||
Out of the box, \thismachine\ presents an 80-column, 32-row text display backed by a $560\times448$-pixel framebuffer with 256 simultaneous colours chosen from a palette of 4096, built-in keyboard and mouse input, and four serial ports for attaching disk drives, modems and other machines. Up to seven expansion cards may be fitted, each mapping a megabyte of its own memory into the address space.
|
||||
|
||||
This guide is one book in two parts. The first part, \emph{\thismachine}, documents the virtual machine itself: its memory map, the Javascript runtime and its built-in libraries, the way peripherals are addressed, and the text, graphics and audio hardware. The second part, \emph{\thedos}, documents the disk operating system that is usually shipped with the machine: how it boots, the commands and applications it provides, how it plays back media, the format in which commands describe themselves, and the libraries it offers to programs of your own.
|
||||
|
||||
This is the documentation for \thismachine\ version \tsvmver\ and the \thedos\ that accompanies it.
|
||||
|
||||
@@ -10,6 +10,6 @@ Copyrighted under the terms of MIT License
|
||||
|
||||
\begin{center}
|
||||
\begin{tabulary}{\textwidth}{ll}
|
||||
Zeroth Edition (for version 1.0): & \thepublishingdate
|
||||
\theedition\ (for version \tsvmver): & \thepublishingdate
|
||||
\end{tabulary}
|
||||
\end{center}
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
\newcommand{\codeline}[1]{%
|
||||
\colorbox{lgrey}{%
|
||||
\begin{tabular*}{\textwidth}{l}%
|
||||
\monofont #1 \\% TODO fill the cell with \hl colour
|
||||
\monofont #1 \\% cell background is provided by the enclosing \colorbox
|
||||
\end{tabular*}%
|
||||
}}
|
||||
|
||||
@@ -203,8 +203,8 @@
|
||||
\newcommand{\thismachine}{TSVM}
|
||||
\newcommand{\thedos}{TVDOS}
|
||||
\newcommand{\tsvmver}{1.2}
|
||||
\newcommand{\theedition}{Zeroth Edition}
|
||||
\newcommand{\thepublishingdate}{0000-00-00}
|
||||
\newcommand{\theedition}{First Edition}
|
||||
\newcommand{\thepublishingdate}{2026-06-06}
|
||||
\newcommand{\oreallypress}{\begingroup\hspace{0.083em}\large\textbf{O'REALLY\raisebox{1ex}{\scriptsize ?}} \large Press\endgroup}
|
||||
|
||||
\newcommand{\argN}[1]{arg\textsubscript{#1}}
|
||||
@@ -247,10 +247,10 @@
|
||||
|
||||
% \input{changesmeta}
|
||||
|
||||
\part{The Virtual Machine}
|
||||
\part{\thismachine: The Virtual Machine}
|
||||
\input{implementation}
|
||||
|
||||
\part{The DOS}
|
||||
\part{\thedos: The Disk Operating System}
|
||||
\input{tvdos}
|
||||
|
||||
\part*{Bibliography}
|
||||
|
||||
677
doc/tvdos.tex
677
doc/tvdos.tex
@@ -7,16 +7,16 @@ All \thedos-related features requires the DOS to be fully loaded.
|
||||
|
||||
\chapter{Bootstrapping}
|
||||
|
||||
\index{boot process}\thedos\ goes through follwing progress to deliver the \code{A:\rs} prompt:
|
||||
\index{boot process}\thedos\ goes through the following progress to deliver the \code{A:\rs} prompt:
|
||||
|
||||
\section{Probing Bootable Devices}
|
||||
The BIOS will probe serial devices to find first bootable drive. If found, port number of the driver is written to the \code{\_BIOS} object, then attempts to load and run the bootloader.
|
||||
The BIOS will probe serial devices to find the first bootable drive. If found, the port number of the drive is written to the \code{\_BIOS} object, then it attempts to load and run the bootloader.
|
||||
|
||||
\section{The Bootloader}
|
||||
The Bootloader is a short program that loads the \code{TVDOS.SYS} file.
|
||||
|
||||
\section{TVDOS.SYS}
|
||||
\thedos.SYS will load system libraries and variables and then will try to run the boot script by executing \code{A:\rs{}AUTOEXEC.BAT}
|
||||
\thedos.SYS loads the system libraries and variables, installs the filesystem and input drivers, and then runs the boot script.
|
||||
|
||||
Boot Procedure:
|
||||
|
||||
@@ -26,21 +26,58 @@ Boot Procedure:
|
||||
\item initialise DOS variables
|
||||
\item install filesystem drivers
|
||||
\item install input device drivers
|
||||
\item install GL using the external file
|
||||
\item execute \code{AUTOEXEC.BAT}
|
||||
\begin{enumerate}
|
||||
\item execute \code{command.js} with proper arguments
|
||||
\item \code{command.js} to initialise \code{shell.*} functions (this includes coreutils and patched version of \code{require})
|
||||
\item \code{command.js} to parse and run \code{AUTOEXEC.BAT}
|
||||
\end{enumerate}
|
||||
\item install \code{GL} using the external file
|
||||
\item run \code{\rs{}commandrc} to set up the environment
|
||||
\item hand the screen to the virtual-console manager, \code{\rs{}tvdos\rs{}VTMGR.SYS}
|
||||
\item when the manager exits, run \code{\rs{}AUTOEXEC.BAT} as a bare fallback shell
|
||||
\end{enumerate}
|
||||
|
||||
\section{AUTOEXEC.BAT}
|
||||
Steps 7--9 are run through \code{command.js}: \thedos.SYS loads \code{command.js}, which initialises the \code{shell.*} functions (the coreutils and a patched \code{require}), and then parses and runs the requested script.
|
||||
|
||||
AUTOEXEC can setup user-specific variables (e.g. keyboard layout) and launch the command shell of your choice, \code{COMMAND} is the most common shell.
|
||||
\section{The Boot Configuration: commandrc and AUTOEXEC.BAT}
|
||||
|
||||
Variables can be set or changed using \textbf{SET} commands.
|
||||
\index{commandrc}\index{AUTOEXEC.BAT}The boot configuration is split across two files with deliberately different jobs. The split exists because \thedos\ runs several independent shell sessions at once (see \emph{Virtual Consoles}): the environment must be established in \emph{every} session, but applications should be launched only \emph{once per session}.
|
||||
|
||||
\code{\rs{}commandrc} is the \textbf{environment} file. It holds \code{set} commands and nothing else, and \thedos.SYS runs it in every context --- the boot console and every virtual-console pane. Because it has no \code{.BAT} extension, the boot block runs it line by line. A typical \code{commandrc} configures the search paths and the keyboard layout:
|
||||
|
||||
\begin{lstlisting}
|
||||
set PATH=\tbas;\hopper\bin;$PATH
|
||||
set INCLPATH=\hopper\include;$INCLPATH
|
||||
set HELPPATH=\hopper\help;$HELPPATH
|
||||
set KEYBOARD=us_qwerty
|
||||
\end{lstlisting}
|
||||
|
||||
\code{\rs{}AUTOEXEC.BAT} is the \textbf{per-console launch} script. It is run once for each console: by each virtual-console pane as it starts, and by the boot console as the fallback once the virtual-console manager has exited. It performs work that must happen per session --- registering the input method, then starting the interactive shell:
|
||||
|
||||
\begin{lstlisting}
|
||||
command -fancy
|
||||
\end{lstlisting}
|
||||
|
||||
Variables are set or changed with the \textbf{set} command. A value may refer to the previous value of a variable with \code{\$NAME}, as the \code{\$PATH} above does, which appends to rather than replaces the existing search path.
|
||||
|
||||
|
||||
|
||||
\chapter{Virtual Consoles}
|
||||
|
||||
\index{virtual consoles}\thedos\ runs up to six independent shell sessions at once, called \textbf{virtual consoles}. Only one is shown on the physical screen at a time; the others keep running in the background. This is managed by \code{VTMGR.SYS}, which the boot process starts automatically (see \emph{Bootstrapping}).
|
||||
|
||||
\section{Switching Consoles}
|
||||
|
||||
\begin{outline}
|
||||
\1\textbf{Alt-1} through \textbf{Alt-6} switch to that console. A console is created the first time it is selected, and re-created if its shell has exited.
|
||||
\1the \code{chvt} \emph{N} command switches to console \emph{N} from within a script or a running shell.
|
||||
\1\textbf{Alt-0} shuts the virtual-console manager down entirely, after which the boot console's \code{AUTOEXEC.BAT} runs as a single bare shell.
|
||||
\end{outline}
|
||||
|
||||
Console 1 is created at boot; consoles 2--6 are created on first use. Each console runs its own \code{command -fancy} shell, with its own working directory and screen, but they all share the same environment set up by \code{commandrc}. The prompt of consoles 2--6 is prefixed with \code{[\emph{N}]} so the active console is always identifiable.
|
||||
|
||||
\section{Concurrency}
|
||||
|
||||
Switching is truly concurrent: a console keeps running even while it is not on screen, and you can switch away from a long-running command and back again without interrupting it. A console that is waiting for keyboard input is parked until it is brought to the foreground.
|
||||
|
||||
\section{Well-behaved Applications}
|
||||
|
||||
Most programs need no special handling to run inside a virtual console, because they draw through the \code{con} library and the \code{print} family, which \thedos\ routes to the correct console automatically. The one exception is a program that writes to the text area \emph{directly} through \code{graphics.getGpuMemBase()}: such a program would paint the physical screen and bleed into whichever console is visible. Applications that need direct text-area access must resolve their writes through a console-aware base address; the bundled applications that do this are already adapted.
|
||||
|
||||
|
||||
\chapter{Coreutils}
|
||||
@@ -50,6 +87,7 @@ Variables can be set or changed using \textbf{SET} commands.
|
||||
\begin{outline}
|
||||
\1\dossynopsis{cat}[file]{Reads a file and pipes its contents to the pipe, or to the console if no pipes are specified.}
|
||||
\1\dossynopsis{cd}[dir]{Change the current working directory. Alias: chdir}
|
||||
\1\dossynopsis{chvt}[N]{Switches to virtual console \emph{N} (1--6). Only meaningful inside a virtual console. See \emph{Virtual Consoles}.}
|
||||
\1\dossynopsis{cls}{Clears the text buffer and the framebuffer if available.}
|
||||
\1\dossynopsis{cp}[from to]{Make copies of the specified file. The source file must not be a directory. Alias: copy}
|
||||
\1\dossynopsis{date}{Prints the system date. Alias: time}
|
||||
@@ -59,42 +97,130 @@ Variables can be set or changed using \textbf{SET} commands.
|
||||
\1\dossynopsis{exit}{Exits the current command processor.}
|
||||
\1\dossynopsis{mkdir}[path]{Creates a directory. Aliase: md}
|
||||
\1\dossynopsis{mv}[from to]{Moves or renames the file. Aliase: move}
|
||||
\1\dossynopsis{panic}{Deliberately raises an error in the command processor. A diagnostic aid.}
|
||||
\1\dossynopsis{rem}{Comment-out the line.}
|
||||
\1\dossynopsis{set}[key=value]{Sets the global variable \code{key} to \code{value}, or displays the list of global variables if no arguments were given.}
|
||||
\1\dossynopsis{ver}{Prints the version of \thedos.}
|
||||
\1\dossynopsis{which}[program]{Reports how a name would resolve: as a shell built-in, or as the full path of the executable found on the \code{PATH}. Alias: where}
|
||||
\end{outline}
|
||||
|
||||
|
||||
|
||||
\chapter{Built-in Apps}
|
||||
|
||||
\index{built-in apps (DOS)}Built-in Applications are the programs shipped with the standard distribution of \thedos\ that is written for users' convenience.
|
||||
\index{built-in apps (DOS)}Built-in Applications are the programs shipped with the standard distribution of \thedos\ that are written for users' convenience.
|
||||
|
||||
This chapter will only briefly list and describe the applications.
|
||||
This chapter lists the general-purpose applications. The applications for playing and converting media have a chapter of their own, \emph{Media Playback and Formats}.
|
||||
|
||||
\section{Shells and Languages}
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{basica}{Invokes a BASIC interpreter stored in the ROM. If no BASIC rom is present, nothing will be done.}
|
||||
\1\dossynopsis{basic}{If your system is bundled with a software-based BASIC, this command will invoke the BASIC interpreter stored in the disk.}
|
||||
\1\dossynopsis{color}{Changes the background and the foreground of the active session.}
|
||||
\1\dossynopsis{command}{The default text-based DOS shell. Call with \code{command -fancy} for more \ae sthetically pleasing looks.}
|
||||
\1\dossynopsis{decodeipf}[file]{Decodes the IPF-formatted image to the framebuffer using the graphics processor.}
|
||||
\1\dossynopsis{drives}{Shows the list of the connected and mounted disk drives.}
|
||||
\1\dossynopsis{edit}[file]{The interactive full-screen text editor.}
|
||||
\1\dossynopsis{encodeipf}[1/2 imagefile ipffile]{Encodes the given image file (.jpg, .png, .bmp, .tga) to the IPF format using the graphics hardware.}
|
||||
\1\dossynopsis{false}{Returns errorlevel 1 upon execution.}
|
||||
\1\dossynopsis{geturl}[url]{Reads contents on the web address and store it to the disk. Requires Internet adapter.}
|
||||
\1\dossynopsis{hexdump}[file]{Prints out the contents of a file in hexadecimal view. Supports pipe.}
|
||||
\1\dossynopsis{less}[file]{Allows user to read the long text, even if they are wider and/or taller than the screen. Supports pipe.}
|
||||
\1\dossynopsis{playmov}[file]{Plays tsvmmov-formatted video. Use -i flag for playback control.}
|
||||
\1\dossynopsis{playmp2}[file]{Plays MP2 (MPEG-1 Audio Layer II) formatted audio. Use -i flag for playback control.}
|
||||
\1\dossynopsis{playpcm}[file]{Plays raw PCM audio. Use -i flag for playback control.}
|
||||
\1\dossynopsis{playwav}[file]{Plays linear PCM/ADPCM audio. Use -i flag for playback control.}
|
||||
\1\dossynopsis{printfile}[file]{Prints out the contents of a textfile with line numbers. Useful for making descriptive screenshots.}
|
||||
\1\dossynopsis{touch}[file]{Updates a file's modification date. New file will be created if the specified file does not exist.}
|
||||
\1\dossynopsis{true}{Returns errorlevel 0 upon execution.}
|
||||
\1\dossynopsis{zfm}{Z File Manager. A two-panel graphical user interface to navigate the system using arrow keys. Hit Z to switch panels.}
|
||||
\1\dossynopsis{command}{The default text-based \thedos\ shell. Call with \code{command -fancy} for a more \ae sthetically pleasing look.}
|
||||
\1\dossynopsis{basica}{Invokes a BASIC interpreter stored in the ROM. If no BASIC ROM is present, nothing is done.}
|
||||
\1\dossynopsis{basic}{If your system is bundled with a software-based BASIC, invokes the BASIC interpreter stored on the disk.}
|
||||
\end{outline}
|
||||
|
||||
\section{Files and Text}
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{edit}[file]{The interactive full-screen text editor.}
|
||||
\1\dossynopsis{zfm}{Z File Manager. A two-panel graphical interface for navigating the system with the arrow keys. Press Z to switch panels.}
|
||||
\1\dossynopsis{less}[file]{Lets the user read long text, even when it is wider and/or taller than the screen. Supports pipes.}
|
||||
\1\dossynopsis{hexdump}[file]{Prints the contents of a file in a hexadecimal view. Supports pipes.}
|
||||
\1\dossynopsis{printfile}[file]{Prints the contents of a text file with line numbers. Useful for making descriptive screenshots.}
|
||||
\1\dossynopsis{touch}[file]{Updates a file's modification date. The file is created if it does not exist.}
|
||||
\1\dossynopsis{tee}[file]{In a pipe, copies the incoming stream to a file \emph{and} passes it on to the next command.}
|
||||
\1\dossynopsis{writeto}[file]{In a pipe, writes the incoming stream to a file.}
|
||||
\end{outline}
|
||||
|
||||
\section{Storage and Archives}
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{drives}{Lists the connected and mounted disk drives.}
|
||||
\1\dossynopsis{defrag}{Defragments the current drive.}
|
||||
\1\dossynopsis{gzip}[file]{Compresses a file in place, or decompresses it with \code{-d}, or writes to standard output with \code{-c}. As with the \code{gzip} library, the actual format used is Zstandard.}
|
||||
\1\dossynopsis{lfs}{Creates and extracts \thedos\ Linear File Strip (\code{.lfs}) archives --- a simple way to bundle a directory tree into one file.}
|
||||
\1\dossynopsis{autorun}[file]{Runs a program or plays a media file directly from a sequential (tape) device.}
|
||||
\end{outline}
|
||||
|
||||
\section{Networking and Packages}
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{geturl}[url]{Reads the contents at a web address and stores it to the disk. Requires an Internet adapter.}
|
||||
\1\dossynopsis{telcom}[port]{A terminal program for talking to a device or modem on a serial port.}
|
||||
\1\dossynopsis{hopper}{The \thedos\ package manager, for installing and managing optional software. Alias: hop.}
|
||||
\end{outline}
|
||||
|
||||
\section{Miscellaneous}
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{synopsis}[program]{Prints a command's one-line summary together with an auto-generated synopsis --- its usage line, arguments, options and constraints --- read from the command's \code{.synopsis} description. With no \code{program} it describes itself. Alias: help.}
|
||||
\1\dossynopsis{color}{Changes the background and foreground colour of the active session.}
|
||||
\1\dossynopsis{true}{Returns errorlevel 0 upon execution.}
|
||||
\1\dossynopsis{false}{Returns errorlevel 1 upon execution.}
|
||||
\end{outline}
|
||||
|
||||
The music tracker and editor (invoked as \code{microtone}) is also bundled, but it --- together with the music format it authors --- is large enough to warrant its own manual and is not covered here.
|
||||
|
||||
|
||||
|
||||
\chapter{Media Playback and Formats}
|
||||
|
||||
\index{media playback (DOS)}\thedos\ can display images, play video and play audio, in several formats. Decoding is hardware-accelerated by the graphics and audio adapters, so even the more sophisticated formats play back smoothly. This chapter is written for someone who wants to \emph{use} these formats; it describes what each is for and which command plays it, not how the codecs work internally.
|
||||
|
||||
Most players accept the \code{-i} flag, which turns on \textbf{interactive mode}: an on-screen progress bar, a visualiser where appropriate, and playback control. Without it, the player simply plays the file and exits. Across the players, holding \textbf{Backspace} stops playback.
|
||||
|
||||
\section{Still Images}
|
||||
|
||||
\index{iPF}The machine's native still-image format is \textbf{iPF} (Interchangeable Picture Format). The graphics hardware can also decode ordinary JPEG, PNG and TGA images directly.
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{decodeipf}[file]{Decodes an iPF image onto the framebuffer.}
|
||||
\1\dossynopsis{encodeipf}[1/2 imagefile ipffile]{Encodes an image file (\code{.jpg}, \code{.png}, \code{.bmp}, \code{.tga}) into iPF, using the graphics hardware. The first argument selects the iPF type (1 or 2).}
|
||||
\end{outline}
|
||||
|
||||
The TAV video format below also have still-picture variants, used for high-fidelity images.
|
||||
|
||||
\section{Video}
|
||||
|
||||
\thedos\ supports three families of video, in rough order of age and sophistication:
|
||||
|
||||
\begin{outline}
|
||||
\1\textbf{MV1} --- the legacy movie format, carrying iPF (or plain palette) frames with MP2 audio. Created on the machine with \code{encodemov} / \code{encodemov2}.
|
||||
\1\textbf{TEV} (\thismachine\ Enhanced Video) --- a modern format with markedly better compression than iPF movies, taking full advantage of the 4096-colour hardware.
|
||||
\1\textbf{TAV} (\thismachine\ Advanced Video) --- the current format and successor to TEV, offering the best compression and image quality.
|
||||
\end{outline}
|
||||
|
||||
TEV and TAV files are prepared on a host computer and copied to the disk for playback; MOV and iPF content can also be produced on the machine itself. Both TEV and TAV are encoded at a chosen \emph{quality level} when they are made, trading file size against fidelity --- as a viewer you simply play the result.
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{playmv1}[file]{Plays a MV1-format movie.}
|
||||
\1\dossynopsis{playtev}[file]{Plays a TEV-format video.}
|
||||
\1\dossynopsis{playtav}[file]{Plays a TAV-format video.}
|
||||
\1\dossynopsis{movprobe}[file]{Prints a movie's properties (dimensions, frame rate, audio, \dots) without playing it.}
|
||||
\1\dossynopsis{playucf}[file]{Plays a chaptered movie (UCF), presenting a chapter selector.}
|
||||
\end{outline}
|
||||
|
||||
\section{Audio}
|
||||
|
||||
\index{MP2}\index{TAD}For sound, \thedos\ plays standard \textbf{MP2} (MPEG-1 Audio Layer II), raw and wave-wrapped PCM, and its own compressed format, \textbf{TAD} (\thismachine\ Advanced Audio). All audio plays back at the hardware's 32\,kHz stereo. As with video, TAD files are prepared on a host computer; MP2 is a widely interchangeable format, and PCM/WAV are uncompressed.
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{playmp2}[file]{Plays MP2 (MPEG-1 Audio Layer II) audio.}
|
||||
\1\dossynopsis{playtad}[file]{Plays TAD audio.}
|
||||
\1\dossynopsis{playpcm}[file]{Plays raw PCM audio.}
|
||||
\1\dossynopsis{playwav}[file]{Plays linear-PCM or ADPCM \code{.wav} audio.}
|
||||
\end{outline}
|
||||
|
||||
\section{Music}
|
||||
|
||||
\index{Taud}\thismachine\ has a native tracker music format, \textbf{Taud}, played by the built-in tracker engine (see the \emph{Audio} chapter in the \thismachine\ part). Unlike the streamed audio formats above, a Taud file is a compact \emph{score} --- samples plus the patterns that sequence them --- so a whole piece of music occupies very little space.
|
||||
|
||||
\begin{outline}
|
||||
\1\dossynopsis{playtaud}[file]{Plays a Taud module, with a text-mode visualiser. An optional second argument selects which song within the file to play. Hold Backspace to exit.}
|
||||
\end{outline}
|
||||
|
||||
Authoring Taud music, and the format itself, are the subject of a separate manual and are not described here.
|
||||
|
||||
|
||||
\chapter{Writing Your Own Apps}
|
||||
@@ -110,6 +236,15 @@ The command line arguments are given via the array of strings named `exec\_args`
|
||||
Index zero holds the name used to invoke the app, and the rest hold the actual arguments.
|
||||
|
||||
|
||||
\section{Describing Your App: the Synopsis File}
|
||||
|
||||
\index{synopsis file}Every command should ship a machine-readable description of its own command-line interface, written in the \thedos\ Synopsis Format (TSF) and described in full in the \emph{Command Synopsis Format} chapter.
|
||||
|
||||
The rule is simple: the synopsis lives \emph{next to} the program, with the program's full filename plus a \code{.synopsis} extension. An app installed as \code{cp.js} in \code{\rs{}tvdos\rs{}bin} therefore ships alongside it as \code{\rs{}tvdos\rs{}bin\rs{}cp.js.synopsis}. \thedos\ uses these files to generate help text and tab-completion, so providing one makes your app a first-class citizen of the system.
|
||||
|
||||
Built-in coreutils have no on-disk executable to sit beside, so their synopses live together in \code{\rs{}tvdos\rs{}synopsis}, named for the command (for example \code{\rs{}tvdos\rs{}synopsis\rs{}cp.synopsis}). Either way, once a synopsis is in place the \code{synopsis} command --- and its \code{help} alias --- will display it, and the shell will tab-complete against it.
|
||||
|
||||
|
||||
\section{Invoking Coreutils on the user Apps}
|
||||
|
||||
DOS coreutils and some of the internal functions can be used on Javascript program.
|
||||
@@ -135,6 +270,254 @@ Due to the non-preemptive nature of the virtual machine, the termination\footnot
|
||||
While- and For-loops are always have such checks injected, but the `read()` is not checked for the termination.
|
||||
|
||||
|
||||
\chapter{Command Synopsis Format}
|
||||
\label{ch:tsf}
|
||||
|
||||
\index{TSF}\index{synopsis format}The \thedos\ Synopsis Format (TSF) is the language in which a command describes its own command-line interface. Every command is expected to ship a TSF document --- a file with the command's full name plus a \code{.synopsis} extension, stored in the same directory as the program. The command \code{cp.js} in \code{\rs{}tvdos\rs{}bin}, for instance, is accompanied by \code{cp.js.synopsis} in that same directory. \thedos\ reads these documents to drive shell completion, help generation, and argument validation.
|
||||
|
||||
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY and OPTIONAL in this chapter are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.
|
||||
|
||||
\section{Scope}
|
||||
|
||||
A TSF document describes a command's grammar: its options and flags, positional arguments, subcommands, argument types, completion sources and validation constraints.
|
||||
|
||||
A TSF document MUST be valid JSON, and MUST be encoded so that its byte stream contains only ASCII characters: any character outside the ASCII range (U+0000--U+007F) MUST be written as a JSON \texttt{\textbackslash u} escape (a backslash, \texttt{u}, and four hexadecimal digits) rather than as a literal multibyte character. Consumers MUST decode such escapes per the JSON specification.
|
||||
|
||||
\section{Design Goals}
|
||||
|
||||
TSF SHALL be machine-readable, human-authorable, and able to support automatic shell completion, automatic help generation, parser generation and GUI generation.
|
||||
|
||||
The structured synopsis grammar SHALL be the sole normative description of command syntax; every other representation, including human-readable usage strings, is treated as output generated from it.
|
||||
|
||||
\section{Document Structure}
|
||||
|
||||
A TSF document SHALL contain one JSON object.
|
||||
|
||||
\begin{lstlisting}
|
||||
{
|
||||
"tsfVersion": "1.0",
|
||||
"name": "cp",
|
||||
"summary": "Copy files and directories",
|
||||
"symbols": {},
|
||||
"synopsis": {}
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Its fields are:
|
||||
|
||||
\begin{tabulary}{\textwidth}{lclL}
|
||||
Field & Req. & Type & Notes \\
|
||||
\hline
|
||||
tsfVersion & yes & string & Version of TSF this document targets. \\
|
||||
name & yes & string & Command name as invoked. \\
|
||||
summary & yes & string & One-line description. \\
|
||||
symbols & yes & object & The symbol table. \\
|
||||
synopsis & yes & object & The synopsis grammar root node. \\
|
||||
description & no & string & Free-form long description for help generation. \\
|
||||
constraints & no & array & Constraint objects. \\
|
||||
metadata & no & object & Free-form, non-normative data for authors and hosts. \\
|
||||
\end{tabulary}
|
||||
|
||||
\section{The Symbol Table}
|
||||
|
||||
All command elements SHALL be declared in the symbol table, and the synopsis grammar SHALL reference them by identifier.
|
||||
|
||||
\begin{lstlisting}
|
||||
"symbols": {
|
||||
"recursive": { "kind": "option", "long": "--recursive", "short": "-r" },
|
||||
"source": { "kind": "positional", "type": "path" }
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Every symbol has a \code{kind}, one of \code{option}, \code{positional}, \code{subcommand} or \code{group}.
|
||||
|
||||
\section{Argument Descriptors}
|
||||
|
||||
An \emph{argument descriptor} describes a single consumed value. The same shape is used in two places: directly on a \code{positional} symbol, and as the \code{value} of an \code{option} symbol.
|
||||
|
||||
\begin{tabulary}{\textwidth}{lclL}
|
||||
Field & Req. & Type & Notes \\
|
||||
\hline
|
||||
type & no & string & One of the built-in types. Defaults to \code{string}. \\
|
||||
name & no & string & Metavar shown in generated usage (e.g.\ FILE, WHEN). \\
|
||||
values & cond. & array & Permitted values; REQUIRED when \code{type} is \code{enum}. \\
|
||||
default & no & any & Default value, for help and GUI prefill. \\
|
||||
validation & no & object & Value-level validation (below). \\
|
||||
completion & no & object & Completion override. \\
|
||||
summary & no & string & Short description of the value. \\
|
||||
\end{tabulary}
|
||||
|
||||
Each entry in \code{values} SHALL be either a bare JSON value or an object \code{\{ "value": v, "summary": s \}}; the optional per-value summary feeds completion hints and help.
|
||||
|
||||
\subsection{Validation}
|
||||
|
||||
The \code{validation} object expresses checks the grammar cannot:
|
||||
|
||||
\begin{tabulary}{\textwidth}{lL}
|
||||
Field & Notes \\
|
||||
\hline
|
||||
pattern & A regular expression the value MUST match (string-like types). \\
|
||||
minimum & Inclusive lower bound (numeric types). \\
|
||||
maximum & Inclusive upper bound (numeric types). \\
|
||||
minLength & Minimum length in characters (string-like types). \\
|
||||
maxLength & Maximum length in characters (string-like types). \\
|
||||
\end{tabulary}
|
||||
|
||||
The regular-expression flavour for \code{pattern} is implementation-defined; authors are RECOMMENDED to keep to a portable subset. As the document is ASCII-only, any non-ASCII character within a pattern MUST be escaped.
|
||||
|
||||
\section{Option Symbols}
|
||||
|
||||
\begin{lstlisting}
|
||||
"output": {
|
||||
"kind": "option",
|
||||
"long": "--output",
|
||||
"short": "-o",
|
||||
"summary": "Write output to FILE",
|
||||
"value": { "name": "FILE", "type": "file", "required": true }
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
\begin{tabulary}{\textwidth}{lclL}
|
||||
Field & Req. & Type & Notes \\
|
||||
\hline
|
||||
kind & yes & string & \code{option}. \\
|
||||
long & cond. & string & Long form, e.g.\ \code{--recursive}. \\
|
||||
short & cond. & string & Short form, e.g.\ \code{-r}. \\
|
||||
summary & no & string & One-line description. \\
|
||||
value & no & object & Argument descriptor. Omit for a bare flag. \\
|
||||
negatable & no & boolean & If true, a \code{--no-<long>} form is also accepted. \\
|
||||
\end{tabulary}
|
||||
|
||||
At least one of \code{long} or \code{short} SHALL exist. The \code{value} object MAY carry a \code{required} field (default true): when false, the argument is optional, as in \code{--color} with or without \code{=WHEN}. How often an option may repeat is expressed in the grammar (via \code{repeat} or \code{oneOrMore}), not by a field on the symbol.
|
||||
|
||||
\section{Positional Symbols}
|
||||
|
||||
A positional symbol is an argument descriptor plus its \code{kind}.
|
||||
|
||||
\begin{lstlisting}
|
||||
"source": { "kind": "positional", "type": "path", "summary": "Source file" }
|
||||
\end{lstlisting}
|
||||
|
||||
Its fields are \code{kind} (\code{positional}) followed by the argument-descriptor fields (\code{type}, \code{name}, \code{values}, \code{default}, \code{validation}, \code{completion}, \code{summary}). Whether a positional is required or optional is expressed in the grammar by wrapping it in \code{optional} or not, keeping a single source of truth.
|
||||
|
||||
\section{Subcommand Symbols}
|
||||
|
||||
\begin{lstlisting}
|
||||
"clone": { "kind": "subcommand", "summary": "Clone repository", "tsf": "git.clone" }
|
||||
\end{lstlisting}
|
||||
|
||||
A subcommand MAY reference an embedded or an external TSF document through its \code{tsf} field; resolution of that reference is implementation-specific.
|
||||
|
||||
\section{Group Symbols}
|
||||
|
||||
A group collects related symbols --- typically options --- so the grammar can refer to them collectively. A group is what backs the conventional \code{[OPTION...]} slot.
|
||||
|
||||
\begin{lstlisting}
|
||||
"commonOptions": {
|
||||
"kind": "group",
|
||||
"summary": "Common options",
|
||||
"members": ["recursive", "force", "verbose"]
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
A group has \code{kind} (\code{group}), a \code{members} array of symbol identifiers, and an optional \code{summary}. A reference to a group is equivalent to a \code{choice} over its members; wrapping that reference in \code{repeat} yields the familiar ``any number of these options, in any order'' behaviour.
|
||||
|
||||
\section{The Synopsis Grammar}
|
||||
|
||||
The \code{synopsis} object describes valid invocations as a tree of nodes. Every node has a \code{type}:
|
||||
|
||||
\begin{tabulary}{\textwidth}{lL}
|
||||
Node & Meaning \\
|
||||
\hline
|
||||
sequence & All children appear in order. Has a \code{children} array. (\code{A B C}) \\
|
||||
choice & Exactly one child appears. Has a \code{children} array. (\code{(A | B | C)}) \\
|
||||
optional & The single \code{child} appears zero or one time. (\code{[A]}) \\
|
||||
repeat & The single \code{child} appears zero or more times. (\code{A...}) \\
|
||||
oneOrMore & The single \code{child} appears at least once; sugar for \code{sequence[A, repeat[A]]}. (\code{A [A...]}) \\
|
||||
reference & References a \code{symbol} by identifier. A reference to a group expands to a \code{choice} over its members. \\
|
||||
\end{tabulary}
|
||||
|
||||
\section{A Complete Example}
|
||||
|
||||
The human-readable form \code{cp [OPTION...] SOURCE DEST} is expressed as:
|
||||
|
||||
\begin{lstlisting}
|
||||
"symbols": {
|
||||
"recursive": { "kind": "option", "long": "--recursive", "short": "-r" },
|
||||
"force": { "kind": "option", "long": "--force", "short": "-f" },
|
||||
"options": { "kind": "group", "members": ["recursive", "force"] },
|
||||
"source": { "kind": "positional", "type": "path", "name": "SOURCE" },
|
||||
"destination": { "kind": "positional", "type": "path", "name": "DEST" }
|
||||
},
|
||||
"synopsis": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{ "type": "repeat", "child": { "type": "reference", "symbol": "options" } },
|
||||
{ "type": "reference", "symbol": "source" },
|
||||
{ "type": "reference", "symbol": "destination" }
|
||||
]
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Because \code{options} is a declared symbol, the \code{[OPTION...]} slot satisfies the rule that every referenced element exists in the symbol table.
|
||||
|
||||
\section{Argument Types}
|
||||
|
||||
The built-in primitive types are \code{string}, \code{integer}, \code{float}, \code{boolean}, \code{path}, \code{file}, \code{directory}, \code{url}, \code{hostname}, \code{user}, \code{group}, \code{command} and \code{enum}.
|
||||
|
||||
A descriptor whose \code{type} is \code{enum} SHALL provide a \code{values} array. The \code{values} array MAY also be supplied for non-\code{enum} types as a soft suggestion list that informs completion without restricting valid input. Unknown types SHALL be interpreted as \code{string}; implementations are RECOMMENDED to emit a diagnostic when they do, since an unknown type is usually an authoring error. Each type carries a default completion behaviour --- \code{path}, \code{file} and \code{directory} complete against the filesystem, \code{user} and \code{group} against the account databases, \code{enum} against its \code{values} --- which a \code{completion} block overrides.
|
||||
|
||||
\section{Completion}
|
||||
|
||||
If a descriptor has no \code{completion} block, completion is derived automatically from its \code{type}. A block overrides that default and names a \code{method}:
|
||||
|
||||
\begin{tabulary}{\textwidth}{lL}
|
||||
method & Notes \\
|
||||
\hline
|
||||
type & Use the default completion implied by the \code{type}. (Implicit when no block is present.) \\
|
||||
enum & Complete from the descriptor's \code{values}. (Implicit when \code{type} is \code{enum}.) \\
|
||||
internal & Use a named provider resolved by the host (a \code{provider} field names it). \\
|
||||
command & Run a command whose output supplies the candidates. \\
|
||||
list & Offer a static inline list of suggestions, without restricting input. \\
|
||||
none & Suppress completion for this value. \\
|
||||
\end{tabulary}
|
||||
|
||||
\section{Constraints}
|
||||
|
||||
Constraints describe relationships not expressible in the grammar, and are listed in the root \code{constraints} array. Symmetric constraints use a \code{symbols} field; asymmetric constraints use \code{subject} and \code{targets}.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{conflicts}{symmetric --- the listed \code{symbols} are mutually exclusive.}
|
||||
\1\inlinesynopsis{requires}{asymmetric --- if \code{subject} is present, every symbol in \code{targets} MUST also be present.}
|
||||
\1\inlinesynopsis{implies}{asymmetric derivation --- if \code{subject} is present, every symbol in \code{targets} is implicitly set (a side effect, not a rejection).}
|
||||
\1\inlinesynopsis{cardinality}{symmetric --- constrains how many of the listed \code{symbols} may appear, via \code{minimum} and \code{maximum}.}
|
||||
\end{outline}
|
||||
|
||||
Three of these (\code{conflicts}, \code{requires}, \code{cardinality}) are validation predicates that decide whether an invocation is well-formed; \code{implies} instead sets a value.
|
||||
|
||||
\begin{lstlisting}
|
||||
"constraints": [
|
||||
{ "type": "conflicts", "symbols": ["stdout", "output"] },
|
||||
{ "type": "cardinality", "symbols": ["create", "extract", "list"],
|
||||
"minimum": 1, "maximum": 1 }
|
||||
]
|
||||
\end{lstlisting}
|
||||
|
||||
\section{Generated Usage and Compatibility}
|
||||
|
||||
Implementations SHOULD generate human-readable usage text from the synopsis grammar; that text is non-authoritative output, the grammar remaining the sole normative description.
|
||||
|
||||
TSF distinguishes additive content, which may be ignored safely, from structural content, which may not. Unknown \emph{fields} on otherwise-valid objects SHALL be ignored, so future minor versions MAY add fields without breaking existing documents or consumers. Unknown \emph{grammar node types} and unknown \emph{symbol kinds} SHALL cause the document to be rejected or to enter an explicitly defined degraded mode, because ignoring them would silently change the set of accepted invocations.
|
||||
|
||||
Authors and hosts that need to attach implementation-specific data SHOULD do so inside the root \code{metadata} object or under field names prefixed with \code{x-}; names without that prefix are reserved for future versions of this specification. Consumers SHOULD report the highest \code{tsfVersion} they support so producers can downgrade gracefully.
|
||||
|
||||
\section{How \thedos\ Uses Synopsis Documents}
|
||||
|
||||
A synopsis document is put to work in three ways. The shell uses it for \emph{tab-completion}: when you press Tab part-way through an argument, the grammar tells the shell whether to offer option flags, a fixed set of \code{enum} values, a subcommand name, or filesystem entries (for the \code{path}, \code{file} and \code{directory} types). The \code{synopsis} command --- and its \code{help} alias --- uses it for \emph{help}, printing the summary, a generated usage line, and the arguments, options and constraints. Authors may further use the \code{validation} and \code{constraints} sections to check an invocation before acting on it.
|
||||
|
||||
\thedos\ finds a command's document the same way it finds the command itself: an app's synopsis sits next to its executable (\code{geturl.js} beside \code{geturl.js.synopsis}), while the built-in coreutils, having no file of their own, keep theirs in \code{\rs{}tvdos\rs{}synopsis}. Aliases resolve to the canonical command's document, so \code{ls} is described by \code{dir.synopsis}. Parsed documents are cached under \code{\rs{}tvdos\rs{}cache\rs{}synopsis} so that repeated completions and help lookups stay fast; the cache is rebuilt on demand and may be deleted at any time.
|
||||
|
||||
|
||||
\chapter{Pipes}
|
||||
|
||||
\index{pipe (DOS)}Pipe is a way to chain the IO of the one program/command into the different programs/commands in series.
|
||||
@@ -230,6 +613,7 @@ Functions:
|
||||
\2\argsynopsis{sread}{returns an empty string}
|
||||
\1\inlinesynopsis{ZERO}{returns zero upon reading}
|
||||
\2\argsynopsis{pread}{returns the specified number of zeros}
|
||||
\1\inlinesynopsis{TMP}{a scratch area for temporary files, addressed as \code{\$:\rs{}TMP\rs{}\dots}. Files written here are not expected to persist.}
|
||||
\1\inlinesynopsis{CON}{manipulates the screen text buffer, disregarding the colours}
|
||||
\2\argsynopsis{pread}{reads the texts as bytes.}
|
||||
\2\argsynopsis{bread}{reads the texts as bytes.}
|
||||
@@ -256,24 +640,34 @@ Functions:
|
||||
|
||||
\subsection{Input Events}
|
||||
|
||||
Input events are Javascript array of: $$ [\mathrm{event\ name,\ arg_1,\ arg_2 \cdots arg_n}] $$, where:
|
||||
An input event is a Javascript array of the form $$ [\mathrm{event\ name,\ arg_1,\ arg_2 \cdots arg_n}] $$ where the event name is one of \textbf{key\_down}, \textbf{key\_change}, \textbf{mouse\_down}, \textbf{mouse\_up}, \textbf{mouse\_move} or \textbf{mouse\_wheel}. Every event ends with the current key-press buffer (\code{keycode0} through \code{keycode7}), so a handler can detect modifier keys held during a mouse action.
|
||||
|
||||
\begin{outline}
|
||||
\1event name --- one of following: \textbf{key\_down}, \textbf{mouse\_down}, \textbf{mouse\_move}
|
||||
\1arguments for \textbf{key\_down}:
|
||||
\2\argsynopsis{\argN{1}}{Key Symbol (string) of the head key}
|
||||
\2\argsynopsis{\argN{1}}{Key symbol (string) of the head key}
|
||||
\2\argsynopsis{\argN{2}}{Repeat count of the key event}
|
||||
\2\argsynopsis{\argN{3}..\argN{10}}{The keycodes of the pressed keys}
|
||||
\1arguments for \textbf{mouse\_down}:
|
||||
\1arguments for \textbf{key\_change} (a key was released):
|
||||
\2\argsynopsis{\argN{1}}{Key symbol (string) of the key that went up}
|
||||
\2\argsynopsis{\argN{2}}{0}
|
||||
\2\argsynopsis{\argN{3}..\argN{10}}{The keycodes of the keys still held down}
|
||||
\1arguments for \textbf{mouse\_down} / \textbf{mouse\_up}:
|
||||
\2\argsynopsis{\argN{1}}{X-position of the mouse cursor}
|
||||
\2\argsynopsis{\argN{2}}{Y-position of the mouse cursor}
|
||||
\2\argsynopsis{\argN{3}}{Always the integer 1.}
|
||||
\2\argsynopsis{\argN{3}}{Button mask: 1 = left, 2 = right, 4 = middle (for \textbf{mouse\_up}, the button that was released)}
|
||||
\2\argsynopsis{\argN{4}..}{The key-press buffer}
|
||||
\1arguments for \textbf{mouse\_move}:
|
||||
\2\argsynopsis{\argN{1}}{X-position of the mouse cursor}
|
||||
\2\argsynopsis{\argN{2}}{Y-position of the mouse cursor}
|
||||
\2\argsynopsis{\argN{3}}{1 if the mouse button is held down (i.e. dragging), 0 otherwise}
|
||||
\2\argsynopsis{\argN{4}}{X-position of the mouse cursor on the previous frame (previous V-blank of the screen)}
|
||||
\2\argsynopsis{\argN{3}}{Currently-held button mask (non-zero while dragging)}
|
||||
\2\argsynopsis{\argN{4}}{X-position of the mouse cursor on the previous frame}
|
||||
\2\argsynopsis{\argN{5}}{Y-position of the mouse cursor on the previous frame}
|
||||
\2\argsynopsis{\argN{6}..}{The key-press buffer}
|
||||
\1arguments for \textbf{mouse\_wheel}:
|
||||
\2\argsynopsis{\argN{1}}{X-position of the mouse cursor}
|
||||
\2\argsynopsis{\argN{2}}{Y-position of the mouse cursor}
|
||||
\2\argsynopsis{\argN{3}}{$-1$ for a wheel notch up, $+1$ for a notch down}
|
||||
\2\argsynopsis{\argN{4}..}{The key-press buffer}
|
||||
\end{outline}
|
||||
|
||||
|
||||
@@ -328,7 +722,8 @@ External libraries are packaged codes with the intention of being re-used by oth
|
||||
External libraries can be stored in following locations:
|
||||
|
||||
\begin{enumerate}
|
||||
\item \code{A:\rs{}tvdos\rs{}include}
|
||||
\item \code{A:\rs{}tvdos\rs{}include}, the home of the libraries bundled with \thedos
|
||||
\item any directory listed in the \code{INCLPATH} variable (configured in \code{commandrc})
|
||||
\item a path relative to the user program
|
||||
\item an absolute path that can be anywhere
|
||||
\end{enumerate}
|
||||
@@ -336,7 +731,7 @@ External libraries can be stored in following locations:
|
||||
and can be loaded by:
|
||||
|
||||
\begin{enumerate}
|
||||
\item \code{let name = require(libraryname)} // no .mjs extension
|
||||
\item \code{let name = require(libraryname)} // no .mjs extension; searches the include directory and INCLPATH
|
||||
\item \code{let name = require(./libraryname)} // the relative path must start with a dot-slash
|
||||
\item \code{let name = require(A:/path/to/library.mjs)} // full path WITH the .mjs extension
|
||||
\end{enumerate}
|
||||
@@ -357,3 +752,197 @@ const BAR = 127
|
||||
// following line exports the function and the variable
|
||||
exports = { foo, BAR }
|
||||
\end{lstlisting}
|
||||
|
||||
|
||||
|
||||
\chapter{Bundled Libraries}
|
||||
|
||||
\index{bundled libraries (DOS)}\thedos\ ships a set of ready-made libraries in \code{A:\rs{}tvdos\rs{}include}. Each is loaded with \code{require} using its base name --- \code{require("fs")}, \code{require("psg")}, and so on --- and returns the object the library exports. This chapter documents each in turn.
|
||||
|
||||
\section{fs --- Filesystem (NodeJS-compatible)}
|
||||
|
||||
\index{fs (library)}\code{fs} wraps the \thedos\ file interface in the familiar NodeJS \code{fs} API. Synchronous (\code{*Sync}) calls, callback-style asynchronous calls, and a \code{promises} namespace are all provided; because the machine has no real concurrency, the ``asynchronous'' calls execute immediately and then invoke the callback or resolve the promise. Binary data is exchanged as \code{Uint8Array}; supplying an encoding (\code{"utf8"}, \code{"binary"}, \dots) exchanges strings instead.
|
||||
|
||||
\begin{lstlisting}
|
||||
let fs = require("fs")
|
||||
let txt = fs.readFileSync("A:/etc/motd", "utf8")
|
||||
fs.writeFileSync("A:/tmp/hello.txt", "hi", "utf8")
|
||||
fs.readdirSync("A:/").forEach(println)
|
||||
\end{lstlisting}
|
||||
|
||||
Frequently used members:
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{readFileSync}[path, options]{reads an entire file.}
|
||||
\1\inlinesynopsis{writeFileSync}[path, data, options]{writes (and truncates) a file.}
|
||||
\1\inlinesynopsis{appendFileSync}[path, data, options]{appends to a file.}
|
||||
\1\inlinesynopsis{existsSync}[path]{whether a path exists.}
|
||||
\1\inlinesynopsis{statSync}[path]{returns a \code{Stats} object.}
|
||||
\1\inlinesynopsis{readdirSync}[path]{lists a directory.}
|
||||
\1\inlinesynopsis{mkdirSync}[path]{creates a directory.}
|
||||
\1\inlinesynopsis{unlinkSync, rmSync, rmdirSync}[path]{remove files / directories.}
|
||||
\1\inlinesynopsis{renameSync, copyFileSync, cpSync}[src, dest]{rename and copy.}
|
||||
\1\inlinesynopsis{openSync, readSync, writeSync, closeSync}{the low-level descriptor calls.}
|
||||
\end{outline}
|
||||
The corresponding callback forms (\code{readFile}, \code{writeFile}, \dots), the \code{promises} namespace, the \code{Stats} and \code{Dirent} classes, and the usual \code{constants} are also exported.
|
||||
|
||||
\section{getopt --- Option Parsing}
|
||||
|
||||
\index{getopt (library)}\code{getopt} is a port of the POSIX \code{getopt()} routine. Construct a \code{BasicParser} from an option string and the argument vector, then call \code{getopt()} repeatedly; each call returns an object \code{\{ option, optarg \}} for the next option, or \code{undefined} at the end. A colon after a letter in the option string means that option takes an argument; long aliases are written in parentheses.
|
||||
|
||||
\begin{lstlisting}
|
||||
let getopt = require("getopt")
|
||||
let parser = new getopt.BasicParser("r(recursive)o:(output)", exec_args)
|
||||
let o
|
||||
while ((o = parser.getopt()) !== undefined) {
|
||||
switch (o.option) {
|
||||
case 'r': /* recursive */ break
|
||||
case 'o': let target = o.optarg; break
|
||||
}
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
\section{gl --- Graphics Library}
|
||||
|
||||
\index{gl (library)}\code{gl} is the same graphics-drawing library installed at boot as the \code{GL} namespace; \code{require("gl")} returns the same set of textures, sprite-sheets and drawing functions. It is documented in full in the \emph{The Graphics Library} chapter.
|
||||
|
||||
\section{net --- Internet Text Fetch}
|
||||
|
||||
\index{net (library)}\code{net} fetches text over the network through an attached HTTP modem, translating ordinary URLs into the form the modem expects.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{isAvailable}[]{true if an HTTP modem is attached.}
|
||||
\1\inlinesynopsis{getHttpDrive}[]{the drive letter bound to the HTTP modem, or null.}
|
||||
\1\inlinesynopsis{fetchText}[url]{fetches a URL and returns the body as a string, or null on failure.}
|
||||
\1\inlinesynopsis{fetchTextOrThrow}[url]{as \code{fetchText}, but throws on failure.}
|
||||
\1\inlinesynopsis{open}[url]{returns a file descriptor backed by the modem, whose \code{sread}/\code{bread} trigger the fetch.}
|
||||
\1\inlinesynopsis{toModemPath}[url]{returns the \code{<drive>:\rs{}<url>} path the fetch would use.}
|
||||
\end{outline}
|
||||
|
||||
\section{pcm --- PCM and ADPCM Decoding}
|
||||
|
||||
\index{pcm (library)}\code{pcm} decodes linear PCM and Microsoft ADPCM into the audio hardware's 8-bit format, resampling to the 32\,kHz hardware rate as needed. It underpins the \code{playpcm} and \code{playwav} players.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{decodeLPCM}[inPtr, outPtr, inputLen, config]{decodes linear PCM. \code{config} gives \code{nChannels}, \code{bitsPerSample}, \code{samplingRate} and \code{blockSize}.}
|
||||
\1\inlinesynopsis{decodeMS\_ADPCM}[inPtr, outPtr, blockSize, config]{decodes one Microsoft-ADPCM block.}
|
||||
\1\inlinesynopsis{HW\_SAMPLING\_RATE}{the hardware sampling rate, 32000.}
|
||||
\end{outline}
|
||||
The sample-conversion helpers \code{s8Tou8}, \code{s16Tou8}, \code{u16Tos16} and \code{randomRound} are exported too.
|
||||
|
||||
\section{psg --- Programmable Sound Generator}
|
||||
|
||||
\index{psg (library)}\code{psg} is a software sound generator: it synthesises classic chiptune waveforms into a buffer and sends the result to a playhead as PCM. It is handy for sound effects and simple music without authoring a full tracker module.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{makeBuffer}[length]{allocates a mixing buffer; \code{makeBufferNative} / \code{freeBufferNative} use native memory.}
|
||||
\1\inlinesynopsis{clearBuffer}[buf, offsetSec, lengthSec]{silences part of a buffer.}
|
||||
\1\inlinesynopsis{makeSquare}{writes a square wave (with duty cycle) into a buffer.}
|
||||
\1\inlinesynopsis{makeTriangle, makeAliasedTriangle, makeAliasedTriangleNES}{triangle-wave variants.}
|
||||
\1\inlinesynopsis{makeNoise}{writes an LFSR noise waveform.}
|
||||
\1\inlinesynopsis{sendBuffer, sendBufferFast}[buf, playhead, offsetSec, lengthSec]{uploads a buffer to a playhead for playback.}
|
||||
\end{outline}
|
||||
The wave generators take an offset, frequency, amplitude, pan and a mixing operation, so several voices can be layered into one buffer.
|
||||
|
||||
\section{taud --- Tracker Module Loading}
|
||||
|
||||
\index{taud (library)}\code{taud} moves Taud music between disk and the tracker hardware. It is intentionally small; authoring Taud music belongs to the separate Taud manual.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{uploadTaudFile}[inFile, songIndex, playhead]{loads one song from a Taud file into the tracker hardware and prepares the given playhead to play it.}
|
||||
\1\inlinesynopsis{captureTrackerDataToFile}[outFile]{writes the current tracker state (samples, instruments, patterns and cue sheet) out to a single-song Taud file.}
|
||||
\end{outline}
|
||||
|
||||
\section{typesetter --- Rich-text Layout}
|
||||
|
||||
\index{typesetter (library)}\code{typesetter} wraps, aligns and justifies text for the console using a small markup language. It returns an array of strings, each padded to the requested width, ready to print.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{typeset}[text, width, opts]{lays out text to the given width (default: the rest of the current row). \code{opts.defaultAlign} is one of \code{l}, \code{c}, \code{r} or \code{j} (justified).}
|
||||
\1\inlinesynopsis{typesetText}[text, width, defaultAlign]{the same, with the width always given explicitly.}
|
||||
\1\inlinesynopsis{expandEntities}[s]{expands the named entities (glyphs, accidentals, arrows, \code{\ }, \dots).}
|
||||
\end{outline}
|
||||
The markup understands \code{<b>}/\code{<s>} for emphasis, \code{<c>}/\code{<r>}/\code{<l>} for per-line alignment, and \code{<o>} for a hanging-indent box anchored at the cursor column.
|
||||
|
||||
\section{wintex --- TUI Windows}
|
||||
|
||||
\index{wintex (library)}\code{wintex} provides a small text-mode window toolkit: framed, titled windows with their own input and drawing handlers, plus scrolling helpers and a ready-made dialog box.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{WindowObject}[x, y, w, h, inputProcessor, drawContents, title, drawFrame]{constructs a window; the two handler functions receive input events and redraw the contents.}
|
||||
\1\inlinesynopsis{showDialog}[opts]{displays a modal dialog (message, buttons, \dots) and returns the user's choice.}
|
||||
\1\inlinesynopsis{scrollVert, scrollHorz}{compute new cursor and scroll positions for scrollable lists and fields.}
|
||||
\end{outline}
|
||||
|
||||
\section{lfs --- Archive Extraction}
|
||||
|
||||
\index{lfs (library)}\code{lfs} extracts \thedos\ Linear File Strip (\code{.lfs}) archives programmatically, transparently inflating any compressed entries.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{extractOne}[archive, filename]{extracts a single named entry and returns its file descriptor (under \code{\$:\rs{}TMP}).}
|
||||
\1\inlinesynopsis{extractAll}[archive]{unpacks the whole archive and returns the directory descriptor.}
|
||||
\end{outline}
|
||||
Both take an optional \code{autoDecompress} flag (default true) and require a relative-path archive.
|
||||
|
||||
\section{mload --- Bulk File Loading}
|
||||
|
||||
\index{mload (library)}\code{mload} is a single function for packaged apps that need to pre-load resources into memory. Given an array of absolute paths, it loads each into memory and returns an array of the corresponding pointers (or \code{null} where a file could not be loaded).
|
||||
|
||||
\begin{lstlisting}
|
||||
let mload = require("mload")
|
||||
let [fontPtr, sheetPtr] = mload(["A:/app/font.bin", "A:/app/sheet.bin"])
|
||||
\end{lstlisting}
|
||||
|
||||
\section{seqread --- Sequential Disk Reading}
|
||||
|
||||
\index{seqread (library)}\code{seqread} reads a file from a serial-connected disk drive as a stream, which is more efficient than random access for whole-file consumption (media players, archive readers).
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{prepare}[fullPath]{opens a file for sequential reading.}
|
||||
\1\inlinesynopsis{readBytes}[length, ptr]{reads bytes into memory (allocating a buffer if no pointer is given).}
|
||||
\1\inlinesynopsis{readInt, readShort, readOneByte, readFourCC, readString}{read typed values from the stream.}
|
||||
\1\inlinesynopsis{skip}[n]{skips ahead; \code{seek}/\code{rewind} reposition the stream.}
|
||||
\1\inlinesynopsis{getReadCount}[]{the number of bytes read so far.}
|
||||
\end{outline}
|
||||
|
||||
\section{seqreadtape --- Sequential Tape Reading}
|
||||
|
||||
\index{seqreadtape (library)}\code{seqreadtape} is the counterpart to \code{seqread} for high-speed tape devices, addressed as \code{\$:\rs{}TAPE0}\dots\code{\$:\rs{}TAPE3}. It reads in large chunks rather than being limited to the serial block size, and exposes the same reading interface (\code{prepare}, \code{readBytes}, \code{readInt}, \dots, \code{skip}, \code{seek}, \code{rewind}) plus \code{close}, \code{isReady} and \code{getCurrentTapeDevice}.
|
||||
|
||||
\section{font --- Character ROM}
|
||||
|
||||
\index{font (library)}\code{font} replaces the displayed character set by uploading a font file into the graphics adapter's character ROM.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{setLowRom}[fullPath]{loads characters 0--127 from a font file.}
|
||||
\1\inlinesynopsis{setHighRom}[fullPath]{loads characters 128--255 from a font file.}
|
||||
\1\inlinesynopsis{resetLowRom, resetHighRom}[]{restore the default character set.}
|
||||
\end{outline}
|
||||
|
||||
\section{keysym --- Key Symbol Constants}
|
||||
|
||||
\index{keysym (library)}\code{keysym} is a table of named key codes (\code{A}, \code{ENTER}, \code{UP}, \code{SHIFT\_LEFT}, \dots) for use with the \code{input} library's events. Note that these are the keycodes carried by \code{input.withEvent} events, which differ from the character codes returned by \code{con.getch}.
|
||||
|
||||
\begin{lstlisting}
|
||||
let key = require("keysym")
|
||||
input.withEvent((e) => {
|
||||
if (e[0] === "key_down" && e.includes(key.ESCAPE)) quit()
|
||||
})
|
||||
\end{lstlisting}
|
||||
|
||||
\section{tbas --- Terran BASIC Runtime}
|
||||
|
||||
\index{tbas (library)}\code{tbas} is the runtime support library for compiled Terran BASIC programs. It supplies the BASIC built-in functions (\code{PRINT}, \code{SIN}, \code{LEFT}, \dots) and the helpers the BASIC compiler emits. It is loaded automatically by a compiled program and is not normally used by hand; it is documented here only for completeness.
|
||||
|
||||
\section{synopsis --- Command Synopsis Loading}
|
||||
|
||||
\index{synopsis (library)}\code{synopsis} loads and caches TSF \code{.synopsis} documents (see the \emph{Command Synopsis Format} chapter) and answers the questions the shell and the \code{synopsis} command ask of them. It resolves a command name to its document --- an app's sits beside the executable, a coreutil's in \code{A:\rs{}tvdos\rs{}synopsis} --- parses it into a model, and caches the result both in memory and on disk.
|
||||
|
||||
\begin{outline}
|
||||
\1\inlinesynopsis{getCompletion}[command, priorArgs, word]{returns the completion candidates for the word being typed: option flags, enum values, subcommand names, and whether the shell should additionally offer filesystem entries.}
|
||||
\1\inlinesynopsis{getModel}[command]{the parsed model --- summary, options, positional arguments, subcommands and constraints --- or null when the command has no synopsis.}
|
||||
\1\inlinesynopsis{getSummary}[command]{the one-line summary, or null.}
|
||||
\1\inlinesynopsis{getUsage}[command]{a generated usage string, such as \code{cp SOURCE DEST}.}
|
||||
\1\inlinesynopsis{resolveSynopsisPath}[command]{the full path of the command's synopsis document, or null.}
|
||||
\1\inlinesynopsis{registerProvider}[name, fn]{registers an \code{internal} completion provider that \code{fn(word)} supplies candidates for; \code{commands} and \code{envvars} are built in.}
|
||||
\1\inlinesynopsis{clearCache}[]{drops the in-memory caches.}
|
||||
\end{outline}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# TVDOS Synopses Format (TSF) Version 1.0 Draft
|
||||
# TVDOS Synopsis Format (TSF) Version 1.0 Draft
|
||||
|
||||
## 1. Scope
|
||||
|
||||
The TVDOS Synopses Format (TSF) is a machine-readable command interface description language.
|
||||
The TVDOS Synopsis Format (TSF) is a machine-readable command interface description language.
|
||||
|
||||
A TSF document describes:
|
||||
|
||||
@@ -16,6 +16,8 @@ A TSF document describes:
|
||||
|
||||
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) when, and only when, they appear in all capitals. Lowercase uses of these words carry their ordinary English meaning and impose no normative requirement.
|
||||
|
||||
TSF lives next to the program, with the program's full filename plus a .synopsis extension. An app installed as `cp.js` in `\tvdos\bin` therefore ships alongside it as `\tvdos\bin\cp.js.synopsis`.
|
||||
|
||||
TSF MUST be valid JSON. A TSF document MUST be encoded such that its byte stream contains only ASCII characters: any character outside the ASCII range (U+0000–U+007F) MUST be represented using a JSON `\uXXXX` escape sequence rather than emitted as a literal multibyte character. Consumers MUST decode such escapes per the JSON specification.
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user