diff --git a/packages/api-model/src/capabilities.ts b/packages/api-model/src/capabilities.ts new file mode 100644 index 00000000..641fdb17 --- /dev/null +++ b/packages/api-model/src/capabilities.ts @@ -0,0 +1,197 @@ +/** + * Capability manifests: what each port's typed `support.capabilities` registry + * says it can do on each tmux release, as the port itself serializes it. + * + * A port never writes this file by hand. Its own build emits it from the + * registry the library refuses with, and its own drift test fails when the + * two disagree. The site reads the manifest from the same checkout and + * revision as that port's API model, resolves every API name in it through + * the model, and computes each table cell from the release fields. Nothing + * here is a list somebody keeps in step. + */ + +export const CAPABILITY_SCHEMA = 'libtmux-capabilities/1' + +/** A tmux command, and the flags of it a capability depends on. */ +export interface CapabilityCommand { + command: string + flags?: string[] +} + +/** How a library behaves where the capability is not available. */ +export type Handling = 'refuse' | 'fallback' | 'repair' + +/** One positive capability, as the port's registry declares it. */ +export interface CapabilityEntry { + /** Stable id shared by every port (`control-mode`, `buffer-delete-named`). */ + id: string + /** Short name for headings. */ + name: string + /** One sentence: what a caller can do with it. */ + summary: string + /** The first tmux release it works on. */ + since: string + /** Exact releases at or above `since` where it does not work. */ + except?: string[] + /** What the library does below `since` or on an excepted release. */ + handling: Handling + /** The API name of the error a refusal raises. Required when `handling` is `refuse`. */ + error?: string + /** Public API names that need this capability, as the port's API model names them. */ + apis: string[] + /** tmux commands the capability concerns. */ + commands: CapabilityCommand[] +} + +/** A release-specific defect the library works around. Not a capability. */ +export interface Workaround { + id: string + name: string + summary: string + /** Exact releases the workaround applies to. */ + releases: string[] + handling: Exclude + apis: string[] + commands: CapabilityCommand[] +} + +/** The floor from the port's `support.versions`. */ +export interface Floor { + minimum: string +} + +export interface CapabilityManifest { + schema: typeof CAPABILITY_SCHEMA + /** The site's port slug. */ + port: string + floor: Floor + capabilities: CapabilityEntry[] + workarounds: Workaround[] +} + +/** A version tmux has printed: `X.Y`, `X.Ya`, `X.Y-rc`, `X.Y-rcN` or `next-X.Y`. */ +const VERSION = /^(?:next-(0|[1-9]\d*)\.(0|[1-9]\d*)|(0|[1-9]\d*)\.(0|[1-9]\d*)([a-z]?)(?:-rc(\d*))?)$/ + +/** A sortable key in tmux's own order: next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya. */ +export function tmuxVersionKey(version: string): number[] { + const m = VERSION.exec(version) + if (!m) throw new TypeError(`not a version tmux prints: ${JSON.stringify(version)}`) + if (m[1] !== undefined) return [Number(m[1]), Number(m[2]), 0, 0, 0] + const letter = m[5] ? m[5].charCodeAt(0) - 96 : 0 + if (m[6] !== undefined) { + if (letter) throw new TypeError(`a candidate carries no patch letter: ${JSON.stringify(version)}`) + return [Number(m[3]), Number(m[4]), 1, m[6] === '' ? 0 : Number(m[6]), 0] + } + return [Number(m[3]), Number(m[4]), 2, 0, letter] +} + +export function compareTmuxVersions(a: string, b: string): number { + const ka = tmuxVersionKey(a) + const kb = tmuxVersionKey(b) + for (let i = 0; i < ka.length; i++) if (ka[i] !== kb[i]) return ka[i] - kb[i] + return 0 +} + +/** What a library does with a capability on one release. */ +export type CellState = 'supported' | 'workaround' | 'fallback' | 'refused' | 'repaired' + +export function capabilityState(entry: CapabilityEntry, release: string): CellState { + const available = compareTmuxVersions(release, entry.since) >= 0 && !(entry.except ?? []).includes(release) + if (available) return 'supported' + if (entry.handling === 'fallback') return 'fallback' + if (entry.handling === 'repair') return 'repaired' + return 'refused' +} + +export function workaroundState(entry: Workaround, release: string): CellState { + if (!entry.releases.includes(release)) return 'supported' + return entry.handling === 'repair' ? 'workaround' : 'refused' +} + +const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/ + +function isString(v: unknown): v is string { + return typeof v === 'string' && v.length > 0 +} + +function checkVersion(errors: string[], where: string, v: unknown): void { + if (!isString(v)) { + errors.push(`${where}: expected a tmux version`) + return + } + try { + tmuxVersionKey(v) + } catch (e) { + errors.push(`${where}: ${(e as Error).message}`) + } +} + +function checkCommands(errors: string[], where: string, v: unknown): void { + if (!Array.isArray(v)) { + errors.push(`${where}: expected an array of commands`) + return + } + v.forEach((c, i) => { + if (!c || typeof c !== 'object' || !isString((c as CapabilityCommand).command)) { + errors.push(`${where}[${i}]: expected { command, flags? }`) + return + } + const flags = (c as CapabilityCommand).flags + if (flags !== undefined && (!Array.isArray(flags) || !flags.every((f) => /^-[A-Za-z]$/.test(f)))) { + errors.push(`${where}[${i}].flags: expected single-letter flags such as "-b"`) + } + }) +} + +function checkNames(errors: string[], where: string, v: unknown): void { + if (!Array.isArray(v) || !v.every(isString)) errors.push(`${where}: expected an array of API names`) +} + +/** + * Validate a manifest without a schema library. Every problem is reported, so + * a port fixing its emitter sees the whole list at once. + */ +export function validateManifest(value: unknown): { manifest?: CapabilityManifest; errors: string[] } { + const errors: string[] = [] + const m = value as Partial | null + if (!m || typeof m !== 'object') return { errors: ['manifest: expected an object'] } + if (m.schema !== CAPABILITY_SCHEMA) errors.push(`schema: expected "${CAPABILITY_SCHEMA}"`) + if (!isString(m.port)) errors.push('port: expected the site port slug') + checkVersion(errors, 'floor.minimum', m.floor?.minimum) + const ids = new Set() + const seen = (where: string, id: unknown) => { + if (!isString(id) || !ID.test(id)) errors.push(`${where}.id: expected a kebab-case id`) + else if (ids.has(id)) errors.push(`${where}.id: "${id}" appears twice`) + else ids.add(id) + } + if (!Array.isArray(m.capabilities)) errors.push('capabilities: expected an array') + else { + m.capabilities.forEach((c, i) => { + const at = `capabilities[${i}]` + seen(at, c?.id) + if (!isString(c?.name)) errors.push(`${at}.name: expected a name`) + if (!isString(c?.summary)) errors.push(`${at}.summary: expected a sentence`) + checkVersion(errors, `${at}.since`, c?.since) + ;(c?.except ?? []).forEach((r, j) => checkVersion(errors, `${at}.except[${j}]`, r)) + if (!['refuse', 'fallback', 'repair'].includes(c?.handling as string)) errors.push(`${at}.handling: expected refuse, fallback or repair`) + if (c?.handling === 'refuse' && !isString(c?.error)) errors.push(`${at}.error: a refusal names its error type`) + checkNames(errors, `${at}.apis`, c?.apis) + checkCommands(errors, `${at}.commands`, c?.commands) + }) + } + if (!Array.isArray(m.workarounds)) errors.push('workarounds: expected an array') + else { + m.workarounds.forEach((w, i) => { + const at = `workarounds[${i}]` + seen(at, w?.id) + if (!isString(w?.name)) errors.push(`${at}.name: expected a name`) + if (!isString(w?.summary)) errors.push(`${at}.summary: expected a sentence`) + if (!Array.isArray(w?.releases) || w.releases.length === 0) errors.push(`${at}.releases: expected at least one release`) + else w.releases.forEach((r, j) => checkVersion(errors, `${at}.releases[${j}]`, r)) + if (!['refuse', 'repair'].includes(w?.handling as string)) errors.push(`${at}.handling: expected refuse or repair`) + checkNames(errors, `${at}.apis`, w?.apis) + checkCommands(errors, `${at}.commands`, w?.commands) + }) + } + return errors.length ? { errors } : { manifest: m as CapabilityManifest, errors } +} diff --git a/packages/api-model/src/index.ts b/packages/api-model/src/index.ts index 78809ba6..2980c112 100644 --- a/packages/api-model/src/index.ts +++ b/packages/api-model/src/index.ts @@ -28,3 +28,17 @@ export { parentInventory } from './parent-inventory.ts' export { navSidecarFor, type NavConfig } from './nav-sidecar.ts' export { parsePythonDoc } from './doc/python.ts' +export { + CAPABILITY_SCHEMA, + capabilityState, + compareTmuxVersions, + tmuxVersionKey, + validateManifest, + workaroundState, + type CapabilityCommand, + type CapabilityEntry, + type CapabilityManifest, + type CellState, + type Handling, + type Workaround, +} from './capabilities.ts' diff --git a/packages/api-model/test/capabilities.test.ts b/packages/api-model/test/capabilities.test.ts new file mode 100644 index 00000000..7626f1c3 --- /dev/null +++ b/packages/api-model/test/capabilities.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest' +import { + CAPABILITY_SCHEMA, + capabilityState, + compareTmuxVersions, + validateManifest, + workaroundState, + type CapabilityEntry, + type Workaround, +} from '../src/capabilities.ts' + +const control: CapabilityEntry = { + id: 'control-mode', + name: 'Control mode', + summary: 'Attach a library-managed control client.', + since: '3.7', + handling: 'fallback', + apis: ['libtmux.Server.observe'], + commands: [{ command: 'attach-session', flags: ['-C'] }], +} + +const runShell: CapabilityEntry = { + id: 'run-shell-capture', + name: 'Captured run-shell output', + summary: 'Read the standard output of run-shell.', + since: '3.3a', + except: ['3.4'], + handling: 'refuse', + error: 'libtmux.CapabilityUnavailable', + apis: ['libtmux.Server.run_shell'], + commands: [{ command: 'run-shell' }], +} + +const breakPane: Workaround = { + id: 'break-pane-exact-3-7', + name: 'break-pane on 3.7', + summary: 'Exact 3.7 ignores the name given to break-pane; a rename follows.', + releases: ['3.7'], + handling: 'repair', + apis: ['libtmux.Pane.break_out'], + commands: [{ command: 'break-pane', flags: ['-n'] }], +} + +const manifest = { + schema: CAPABILITY_SCHEMA, + port: 'py', + floor: { minimum: '3.3a' }, + capabilities: [control, runShell], + workarounds: [breakPane], +} + +describe('tmux version order', () => { + it('ranks development builds and candidates below the release they name', () => { + const ordered = ['next-3.7', '3.7-rc', '3.7-rc3', '3.7', '3.7a', '3.8', 'next-3.9', '3.10'] + expect([...ordered].reverse().sort(compareTmuxVersions)).toEqual(ordered) + }) + + it('refuses forms tmux never prints', () => { + for (const v of ['master', '3.3a-master', 'next-3.3a', '3.3a-rc1', '03.3a', 'openbsd-7.6']) { + expect(() => compareTmuxVersions(v, '3.3a'), v).toThrow(TypeError) + } + }) +}) + +describe('cells', () => { + it('follows since, except and handling', () => { + expect(capabilityState(control, '3.6')).toBe('fallback') + expect(capabilityState(control, '3.7')).toBe('supported') + expect(capabilityState(control, '3.7-rc')).toBe('fallback') + expect(capabilityState(runShell, '3.3a')).toBe('supported') + expect(capabilityState(runShell, '3.4')).toBe('refused') + expect(capabilityState(runShell, '3.5')).toBe('supported') + }) + + it('marks only the releases a workaround names', () => { + expect(workaroundState(breakPane, '3.7')).toBe('workaround') + expect(workaroundState(breakPane, '3.7a')).toBe('supported') + }) +}) + +describe('validateManifest', () => { + it('accepts a complete manifest', () => { + const { manifest: m, errors } = validateManifest(manifest) + expect(errors).toEqual([]) + expect(m?.capabilities.map((c) => c.id)).toEqual(['control-mode', 'run-shell-capture']) + }) + + const broken: [string, unknown, string][] = [ + ['wrong schema', { ...manifest, schema: 'libtmux-capabilities/0' }, 'schema'], + ['unprintable floor', { ...manifest, floor: { minimum: 'master' } }, 'floor.minimum'], + ['duplicate id', { ...manifest, workarounds: [{ ...breakPane, id: 'control-mode' }] }, 'appears twice'], + ['refusal without an error type', { ...manifest, capabilities: [{ ...runShell, error: undefined }] }, 'names its error type'], + ['non-kebab id', { ...manifest, capabilities: [{ ...control, id: 'ControlMode' }] }, 'kebab-case'], + ['bad since', { ...manifest, capabilities: [{ ...control, since: '3.7-dev' }] }, 'since'], + ['bad flag', { ...manifest, capabilities: [{ ...control, commands: [{ command: 'attach-session', flags: ['C'] }] }] }, 'flags'], + ['empty workaround', { ...manifest, workarounds: [{ ...breakPane, releases: [] }] }, 'releases'], + ] + it.each(broken)('rejects a manifest with %s', (_label, value, message) => { + const { manifest: m, errors } = validateManifest(value) + expect(m).toBeUndefined() + expect(errors.join('\n')).toContain(message) + }) +}) diff --git a/scripts/gen-capabilities.mjs b/scripts/gen-capabilities.mjs new file mode 100644 index 00000000..8091fadd --- /dev/null +++ b/scripts/gen-capabilities.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** + * Read each port's capability manifest and write site/src/data/capabilities.json. + * + * A port's `capabilities.json` is emitted by that port from the typed + * `support.capabilities` registry its library refuses with, and the port's own + * drift test keeps the two equal. This script does not interpret prose or + * keep a table: it validates each manifest, resolves every API name in it + * through the port's generated API model, and records the tmux releases the + * matrix columns come from. A name that does not resolve fails the run, so a + * renamed method cannot leave a capability pointing at nothing. + * + * Releases come from tmux's own refs: final tags, the version a release + * branch names in `configure.ac` (3.7d shipped from a branch before it was + * tagged), and the `next-X.Y` that master names. Only releases at or above the + * lowest floor any manifest declares are kept. + * + * Usage: node scripts/gen-capabilities.mjs [--port ] [--manifest =] + * [--tmux ] [--out ] [--check] + */ +import { execFileSync } from 'node:child_process' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { PORTS } from '../site/src/lib/ports.ts' +import { SymbolIndex } from '../packages/api-model/src/link.ts' +import { compareTmuxVersions, tmuxVersionKey, validateManifest } from '../packages/api-model/src/capabilities.ts' + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const args = process.argv.slice(2) +const value = (flag, fallback) => (args.includes(flag) ? args[args.indexOf(flag) + 1] : fallback) +const values = (flag) => args.flatMap((a, i) => (a === flag ? [args[i + 1]] : [])) +const expand = (p) => (p.startsWith('~/') ? join(homedir(), p.slice(2)) : p) +const checking = args.includes('--check') +const only = value('--port') +const out = resolve(value('--out', join(repoRoot, 'site/src/data/capabilities.json'))) +const tmuxCheckout = expand(value('--tmux', '~/study/c/tmux')) +const modelDir = resolve(value('--models', join(repoRoot, 'site/src/data/api'))) + +/** `--manifest py=path/to/capabilities.json` overrides where a port's manifest is read. */ +const overrides = Object.fromEntries(values('--manifest').map((pair) => pair.split('='))) + +/** The manifest sits at the root of the port's checkout, emitted and drift-checked there. */ +function manifestPath(port) { + return overrides[port.slug] ?? join(expand(port.checkout), 'capabilities.json') +} + +function git(...a) { + return execFileSync('git', ['-C', tmuxCheckout, ...a], { encoding: 'utf8' }).trim() +} + +/** Every tmux release a reader can run, from tmux's own refs. */ +function tmuxReleases() { + const versions = new Set() + const add = (v) => { + try { + tmuxVersionKey(v) + versions.add(v) + } catch { + // A ref that names something tmux never prints (a topic branch) is not a release. + } + } + for (const tag of git('tag', '-l').split('\n')) if (/^\d+\.\d+[a-z]?$/.test(tag)) add(tag) + const refs = git('for-each-ref', '--format=%(refname:short)', 'refs/remotes').split('\n') + const named = (ref) => /AC_INIT\(\[tmux\], ([^)]+)\)/.exec(git('show', `${ref}:configure.ac`))?.[1] + for (const ref of refs.filter((r) => /\/release_[\d.]+[a-z]?$/.test(r))) { + const v = named(ref) + if (v && /^\d+\.\d+[a-z]?$/.test(v)) add(v) + } + const master = refs.find((r) => r === 'upstream/master') ?? refs.find((r) => r === 'origin/master') + const next = master && named(master) + if (next?.startsWith('next-')) add(next) + return [...versions].sort(compareTmuxVersions) +} + +/** Resolve a manifest's API name to the public id the reference pages use. */ +function resolver(slug) { + const file = join(modelDir, `${slug}.json`) + if (!existsSync(file)) return undefined + const model = JSON.parse(readFileSync(file, 'utf8')) + const index = new SymbolIndex(model.symbols, () => '', slug) + return (name) => { + const target = index.resolve(name) + return target?.symbol ? (target.symbol.publicId ?? target.symbol.id) : undefined + } +} + +const errors = [] +const ports = {} +for (const port of PORTS) { + if (only && port.slug !== only) continue + const path = manifestPath(port) + if (!existsSync(path)) continue + const { manifest, errors: invalid } = validateManifest(JSON.parse(readFileSync(path, 'utf8'))) + if (!manifest) { + errors.push(...invalid.map((e) => `${port.slug}: ${e}`)) + continue + } + if (manifest.port !== port.slug) errors.push(`${port.slug}: manifest names port "${manifest.port}"`) + const resolve = resolver(port.slug) + if (!resolve) { + errors.push(`${port.slug}: no API model at ${join(modelDir, `${port.slug}.json`)}; run gen-api-model first`) + continue + } + const link = (where, name) => { + const id = resolve(name) + if (!id) errors.push(`${port.slug}: ${where} names "${name}", which is not in the API model`) + return id ?? name + } + const entries = (list, kind) => list.map((e) => ({ + ...e, + apis: e.apis.map((name) => link(`${kind} ${e.id}`, name)), + ...(e.error ? { error: link(`${kind} ${e.id} error`, e.error) } : {}), + })) + ports[port.slug] = { + revision: (() => { + try { + return execFileSync('git', ['-C', dirname(path), 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() + } catch { + return undefined + } + })(), + floor: manifest.floor, + capabilities: entries(manifest.capabilities, 'capability'), + workarounds: entries(manifest.workarounds, 'workaround'), + } +} + +if (errors.length) { + console.error(errors.join('\n')) + process.exit(1) +} + +const floors = Object.values(ports).map((p) => p.floor.minimum) +const lowest = floors.sort(compareTmuxVersions)[0] +const releases = lowest ? tmuxReleases().filter((v) => compareTmuxVersions(v, lowest) >= 0) : [] +const data = `${JSON.stringify({ generated: 'scripts/gen-capabilities.mjs', releases, ports }, null, 2)}\n` + +if (checking) { + const current = existsSync(out) ? readFileSync(out, 'utf8') : '' + if (current !== data) { + console.error(`${out} is stale; run node scripts/gen-capabilities.mjs`) + process.exit(1) + } +} else { + writeFileSync(out, data) + console.log(`wrote ${out}: ${Object.keys(ports).length} ports, ${releases.length} releases`) +} diff --git a/scripts/test-all.sh b/scripts/test-all.sh index f5cbbc02..a3a07570 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -147,6 +147,12 @@ node scripts/gen-shell-ports.negative.mjs step 'mcp tool matrix' node scripts/gen-mcp-tools.mjs --check +# /reference/capabilities/ and the per-command tables are generated from each +# port's capabilities.json, which the port emits from its typed registry. +# Every API name in a manifest must resolve in that port's model. +step 'capability manifests' +node scripts/gen-capabilities.mjs --check + # Each port's own MCP server, built and run to compare its live tool, # resource and prompt schemas against the committed snapshot in # site/src/data/mcp-protocol/. Skips when the sibling checkouts are absent, diff --git a/site/src/components/api/ApiCapabilities.astro b/site/src/components/api/ApiCapabilities.astro new file mode 100644 index 00000000..52170ba5 --- /dev/null +++ b/site/src/components/api/ApiCapabilities.astro @@ -0,0 +1,136 @@ +--- +/** + * tmux capabilities, generated from each port's own registry. + * + * In a port build: that port's capabilities against every tmux release from + * its floor, with the API each one gates. In the root build: one map of every + * capability against every port that publishes a manifest. No row or cell is + * written here; see site/src/lib/capabilities.ts. + */ +import CapabilityMatrix from '../capabilities/CapabilityMatrix.astro' +import DocsLayout from '../../layouts/DocsLayout.astro' +import { DEFAULT_LOCALE, localeRoot } from '../../i18n/locales' +import { CONTRIBUTE_BRANCH, CONTRIBUTE_REPO } from '../../i18n/contribute' +import { PORT_BY_SLUG, portPageUrl, referenceUrl } from '../../lib/ports' +import { buildTarget } from '../../lib/versions' +import { tmuxCommandSources } from '../../lib/tmux-reference' +import { + capabilityFloor, + capabilityPorts, + capabilityRevision, + mapRows, + portColumns, + portRows, + releaseGroups, + type Row, +} from '../../lib/capabilities' + +interface Props { port?: string; version?: string } +const { port, version = buildTarget(process.env).version } = Astro.props as Props +const name = port ? PORT_BY_SLUG[port].name : 'libtmux' +const library = port ? PORT_BY_SLUG[port].projectName : 'libtmux' +const referenceBase = port ? referenceUrl(PORT_BY_SLUG[port], version, 'core') : `${localeRoot(DEFAULT_LOCALE)}reference/` +const pagePath = 'reference/capabilities' +const pageUrl = `${referenceBase}capabilities/` +const title = port ? `${library} tmux capabilities` : 'tmux capabilities' +const description = port + ? `What ${library} can do on each tmux release, generated from its capability registry.` + : 'What each libtmux port can do on each tmux release, generated from each port\'s capability registry.' + +const floor = port ? capabilityFloor(port) : undefined +const revision = port ? capabilityRevision(port) : undefined +const { capabilities, workarounds } = port ? portRows(port) : { capabilities: mapRows(), workarounds: [] as Row[] } +const published = capabilityPorts() +const commandHref = (command: string) => tmuxCommandSources('latest', command)[0]?.href + +const sections = [...capabilities, ...workarounds] +const headings = [ + ...(capabilities.length ? [{ depth: 2, slug: 'capabilities', text: 'Capabilities' }] : []), + ...(workarounds.length ? [{ depth: 2, slug: 'workarounds', text: 'Release workarounds' }] : []), + ...sections.map((r) => ({ depth: 3, slug: r.id, text: r.label })), +] +--- + + + {port && floor ? ( +

+ {library} (libtmux for {name}) requires tmux {floor} or newer. Each capability below needs a newer + tmux release, or avoids one release. Before it sends a command, {library} checks the version of + the running tmux server. The tables and every link on this page are generated from the library's + capability registry{revision && <> at {revision.slice(0, 12)}}. +

+ ) : !port && published.length > 0 ? ( +

+ Every libtmux port supports tmux from its floor up. Each capability below needs a newer release on + some ports; a cell gives the release a port needs. Rows and cells are generated from each port's + capability registry, so a port appears here once it publishes one. +

+ ) : null} + + {published.length === 0 ? ( +

No port publishes a capability registry on this build yet.

+ ) : port && !floor ? ( +

{library} does not publish a capability registry on this build yet.

+ ) : ( + <> +

Capabilities

+ + {workarounds.length > 0 && ( + <> +

Release workarounds

+

A tmux release that gets one command wrong. {library} repairs the result, or refuses the call, on that release only.

+ + + )} + {sections.map((row) => ( +
+

{row.label}

+

{row.summary}

+
+
tmux
+
{row.requirement}
+ {row.error && (<>
Refusal
{row.error.href ? {row.error.label} : {row.error.label}}
)} + {row.apis.length > 0 && ( + <> +
API
+
{row.apis.map((api) => api.href ? {api.label} : {api.label})}
+ + )} + {row.commands.length > 0 && ( + <> +
Commands
+
{row.commands.map((c) => { + const href = commandHref(c.command) + const text = [c.command, ...(c.flags ?? [])].join(' ') + return href ? {text} : {text} + })}
+ + )} +
+
+ ))} + + )} +
diff --git a/site/src/components/api/ApiReference.astro b/site/src/components/api/ApiReference.astro index c2287e0f..50d5586b 100644 --- a/site/src/components/api/ApiReference.astro +++ b/site/src/components/api/ApiReference.astro @@ -21,6 +21,9 @@ import { pagePortsFor } from '../../lib/page-ports-for' import { pageBreadcrumbs } from '../../lib/sidebar' import BaseLayout from '../../layouts/BaseLayout.astro' import { withRoot } from '../../lib/site-root' +import CommandSupport from '../capabilities/CommandSupport.astro' +import { commandRows } from '../../lib/capabilities' +import { tmuxSourceCommands } from '../../lib/tmux-reference' import { API_MODELS, API_NAV, PORT_NAME, indexFor, isMemberOwner, ownersOf, pageSlug, referenceAlternatives, topLevelTypesOf } from '../../lib/api-models' import { PORT_BY_SLUG, referenceUrl } from '../../lib/ports' import { productApiHref } from '../../lib/product-api' @@ -357,6 +360,9 @@ const pageSections: ApiSection[] = glossary ? apiGlossaryHeadings(glossaryContex ...(otherInherited.length ? [{ id: 'api-inherited', label: 'Inherited members', count: otherInherited.length }] : []), ] : [] const relationshipSections = owner && model ? apiRelationshipSections(model, owner) : [] +const tmuxCommandNames = source && owner ? tmuxSourceCommands(source.version, owner).map((command) => command.name) : [] +const supportedCommands = tmuxCommandNames.filter((name) => commandRows(name).length > 0) +if (supportedCommands.length) pageSections.push({ id: 'api-libtmux-support', label: 'Support in libtmux', count: supportedCommands.reduce((n, name) => n + commandRows(name).length, 0) }) const paths = (source?.paths ?? []).filter((path) => !owner || path.symbols.some((symbol) => symbol.id === owner.id)) if (relationshipSections.some((section) => ['api-calls', 'api-called-by'].includes(section.id))) pageSections.push({ id: 'api-call-network', label: 'Call neighborhood' }) if (paths.length) pageSections.push({ id: 'api-source-paths', label: 'Paths through the source', count: paths.length }) @@ -827,6 +833,7 @@ const bases = owner && 'extends' in owner ? owner.extends ?? [] : [] )} + {supportedCommands.map((name) => )} {related.length > 0 &&