doc update/command synopses

This commit is contained in:
minjaesong
2026-06-06 21:59:18 +09:00
parent df16b99ba5
commit 3444bdf63b
49 changed files with 2145 additions and 69 deletions

View 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" }
}

View File

@@ -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 }
}

View File

@@ -0,0 +1,7 @@
{
"tsfVersion": "1.0",
"name": "drives",
"summary": "List connected and mounted disk drives",
"symbols": {},
"synopsis": { "type": "sequence", "children": [] }
}

View 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" }
}
}

View 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" }
}

View 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" }
]
}
}

View File

@@ -0,0 +1 @@
synopsis $0

View 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" }
}
}

View 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" }
}
}

View 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"] }
]
}

View 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" }
}

View 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" } }
]
}
}

View 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"] }
]
}

View 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" } }
]
}
}

View 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" } }
]
}
}

View 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" } }
]
}
}

View 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" }
}

View 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

View 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" }
}
}

View 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" }
}

View 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" }
]
}
}

View 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" }
}