diff --git a/README.md b/README.md index 51b7eba..c323c62 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,8 @@ Status of every feature shipped. ✅ = implemented, ⬜ = roadmap. Section ancho ### Templates - ✅ [`save-template`](#templates) · [`apply-template`](#templates) — extract any segment as reusable JSON; restamp with new timing / position / text -- ✅ 3 templates ship in [`templates/`](./templates/): `gold-title`, `end-card`, `subscribe-cta` +- ✅ [`templates`](#templates) — List available templates that can be used +- ✅ 6 templates ship in [`templates/`](./templates/): `gold-title`, `end-card`, `subscribe-cta`, `hook-question`, `lower-third`, `caption-pop` ### Import & discovery - ✅ [`import-srt`](#import-srt-subtitles-phase-3) — one cue per text segment; file, stdin, or `--style-ref` mirror diff --git a/src/index.ts b/src/index.ts index fb69075..1f987c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { parseAss } from "./ass.js"; import { captionDraft } from "./caption.js"; import { removeChroma, setChroma } from "./chroma.js"; @@ -250,6 +252,9 @@ Templates: apply-template [text override] Stamp a template into a project at the given time Options: --x --y (override position) + templates + Show available templates in the template library. + Use -H for a table. Project: cut --out @@ -1927,6 +1932,48 @@ function cmdDoctor(flags: Flags): boolean { return report.ok; } +function cmdTemplates(flags: Flags): void { + const cliDir = path.dirname(fileURLToPath(import.meta.url)); + const templatesPath = path.join(cliDir, "..", "templates"); + + if (!existsSync(templatesPath)) { + die(`Templates directory not found: ${templatesPath}`); + } + + const descriptions: Record = { + "caption-pop": "word-highlight pop captions", + "lower-third": "name/title lower third", + "hook-question": "opening hook question card", + "gold-title": "gold title card", + "end-card": "end / outro card", + "subscribe-cta": "subscribe call-to-action", + }; + + const entries = readdirSync(templatesPath) + .filter((f) => f.endsWith(".json")) + .map((f) => { + const slug = path.basename(f, ".json"); + return { + slug, + description: descriptions[slug] ?? slug.replace(/-/g, " "), + }; + }); + + if (flags.human) { + if (entries.length === 0) { + console.log("No bundled templates found."); + return; + } + console.log(`${"Slug".padEnd(33)} Description`); + for (const e of entries) { + console.log(`${e.slug.padEnd(33)} ${e.description}`); + } + process.stderr.write(`\n${entries.length} templates\n`); + } else { + out(entries, flags); + } +} + function getCliVersion(): string { const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); @@ -1980,6 +2027,12 @@ async function main(): Promise { process.exit(0); } + // `templates` list all available templates + if (cmd === "templates") { + cmdTemplates(flags); + process.exit(0); + } + // init doesn't need an existing project if (cmd === "init") { const name = projectPath; // positional[1] is the name for init diff --git a/test/showTemplates.test.mjs b/test/showTemplates.test.mjs new file mode 100644 index 0000000..65b3b2f --- /dev/null +++ b/test/showTemplates.test.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { spawnCli } from "./helpers/spawn-cli.mjs"; + +describe("capcut available templates", () => { + it("lists available templates as JSON by default", () => { + const r = spawnCli(["templates"]); + + assert.equal(r.status, 0, `stderr: ${r.stderr}`); + assert.ok(r.json, "stdout should be valid JSON"); + assert.ok(Array.isArray(r.json)); + + const slugs = r.json.map((t) => t.slug); + + assert.ok(slugs.includes("caption-pop")); + assert.ok(slugs.includes("lower-third")); + assert.ok(slugs.includes("hook-question")); + assert.ok(slugs.includes("gold-title")); + assert.ok(slugs.includes("end-card")); + assert.ok(slugs.includes("subscribe-cta")); + }); + + it("renders a human-readable layout with -H", () => { + const r = spawnCli(["templates", "-H"]); + + assert.equal(r.status, 0); + + assert.match(r.stdout, /Slug/); + assert.match(r.stdout, /caption-pop/); + assert.match(r.stdout, /gold-title/); + }); +});