Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
- ✅ [`template`](#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
Expand Down
55 changes: 54 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env node

import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync, readdirSync } 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";
Expand Down Expand Up @@ -250,6 +252,9 @@ Templates:
apply-template <project> <template.json> <start> <duration> [text override]
Stamp a template into a project at the given time
Options: --x <n> --y <n> (override position)
templates
Show available templates in the template library.
Use -H for a table.

Project:
cut <project> <start> <end> --out <path>
Expand Down Expand Up @@ -1925,6 +1930,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<string, string> = {
"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);
}
}

// --- Main ---

async function main(): Promise<void> {
Expand Down Expand Up @@ -1967,6 +2014,12 @@ async function main(): Promise<void> {
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
Expand Down
32 changes: 32 additions & 0 deletions test/showTemplates.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});
});