1 | #!/usr/bin/env node
|
2 | "use strict";
|
3 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
4 | return new (P || (P = Promise))(function (resolve, reject) {
|
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9 | });
|
10 | };
|
11 | Object.defineProperty(exports, "__esModule", { value: true });
|
12 | const fs = require("fs");
|
13 | const chalk_1 = require("chalk");
|
14 | const execa = require("execa");
|
15 | const guess_terminal_1 = require("guess-terminal");
|
16 | const macosAppConfig = require("macos-app-config");
|
17 | const os = require("os");
|
18 | const path = require("path");
|
19 | const tempy = require("tempy");
|
20 | const parsers = require("term-schemes");
|
21 | const commandExists = require("command-exists");
|
22 | const meow = require("meow");
|
23 | const plist = require("plist");
|
24 | const fetch = require("node-fetch");
|
25 | const getStdin = require("get-stdin");
|
26 | const { render } = require("svg-term");
|
27 | const sander = require("@marionebl/sander");
|
28 | const SVGO = require("svgo");
|
29 | withCli(main, `
|
30 | Usage
|
31 | $ svg-term [options]
|
32 |
|
33 | Options
|
34 | --at timestamp of frame to render in ms [number]
|
35 | --cast asciinema cast id to download [string], required if no stdin provided [string]
|
36 | --command command to record [string]
|
37 | --from lower range of timeline to render in ms [number]
|
38 | --height height in lines [number]
|
39 | --help print this help [boolean]
|
40 | --in json file to use as input [string]
|
41 | --no-cursor disable cursor rendering [boolean]
|
42 | --no-optimize disable svgo optimization [boolean]
|
43 | --out output file, emits to stdout if omitted, [string]
|
44 | --padding distance between text and image bounds, [number]
|
45 | --padding-x distance between text and image bounds on x axis [number]
|
46 | --padding-y distance between text and image bounds on y axis [number]
|
47 | --profile terminal profile file to use, requires --term [string]
|
48 | --term terminal profile format [iterm2, xrdb, xresources, terminator, konsole, terminal, remmina, termite, tilda, xcfe], requires --profile [string]
|
49 | --to upper range of timeline to render in ms [number]
|
50 | --width width in columns [number]
|
51 | --window render with window decorations [boolean]
|
52 |
|
53 | Examples
|
54 | $ cat rec.json | svg-term
|
55 | $ svg-term --cast 113643
|
56 | $ svg-term --cast 113643 --out examples/parrot.svg
|
57 | `, {
|
58 | boolean: ['cursor', 'help', 'optimize', 'version', 'window'],
|
59 | string: ['at', 'cast', 'command', 'from', 'height', 'in', 'out', 'padding', 'padding-x', 'padding-y', 'profile', 'term', 'to', 'width'],
|
60 | default: {
|
61 | cursor: true,
|
62 | optimize: true,
|
63 | window: false
|
64 | }
|
65 | });
|
66 | function main(cli) {
|
67 | return __awaiter(this, void 0, void 0, function* () {
|
68 | const error = cliError(cli);
|
69 | if ('command' in cli.flags && !(yield command('asciinema'))) {
|
70 | throw error([
|
71 | `svg-term: asciinema must be installed when --command is specified.`,
|
72 | ` See instructions at: https://asciinema.org/docs/installation`
|
73 | ].join('\n'));
|
74 | }
|
75 | const input = yield getInput(cli);
|
76 | if (!input) {
|
77 | throw error(`svg-term: either stdin, --cast, --command or --in are required`);
|
78 | }
|
79 | const malformed = ensure(["height", "width"], cli.flags, (name, val) => {
|
80 | if (!(name in cli.flags)) {
|
81 | return null;
|
82 | }
|
83 | const candidate = parseInt(val, 10);
|
84 | if (isNaN(candidate)) {
|
85 | return new TypeError(`${name} expected to be number, received "${val}"`);
|
86 | }
|
87 | return null;
|
88 | });
|
89 | if (malformed.length > 0) {
|
90 | throw error(`svg-term: ${malformed.map(m => m.message).join("\n")}`);
|
91 | }
|
92 | const missingValue = ensure(["cast", "out", "profile"], cli.flags, (name, val) => {
|
93 | if (!(name in cli.flags)) {
|
94 | return null;
|
95 | }
|
96 | if (name === "cast" && typeof val === "number") {
|
97 | return null;
|
98 | }
|
99 | if (typeof val === "string") {
|
100 | return null;
|
101 | }
|
102 | return new TypeError(`${name} expected to be string, received "${val}"`);
|
103 | });
|
104 | if (missingValue.length > 0) {
|
105 | throw error(`svg-term: ${missingValue.map(m => m.message).join("\n")}`);
|
106 | }
|
107 | const shadowed = ensure(["at", "from", "to"], cli.flags, (name, val) => {
|
108 | if (!(name in cli.flags)) {
|
109 | return null;
|
110 | }
|
111 | const v = typeof (val) === "number" ? val : parseInt(val, 10);
|
112 | if (isNaN(v)) {
|
113 | return new TypeError(`${name} expected to be number, received "${val}"`);
|
114 | }
|
115 | if (name !== "at" && !isNaN(parseInt(cli.flags.at, 10))) {
|
116 | return new TypeError(`--at flag disallows --${name}`);
|
117 | }
|
118 | return null;
|
119 | });
|
120 | if (shadowed.length > 0) {
|
121 | throw error(`svg-term: ${shadowed.map(m => m.message).join("\n")}`);
|
122 | }
|
123 | const term = 'term' in cli.flags ? cli.flags.term : guess_terminal_1.guessTerminal();
|
124 | const profile = 'profile' in cli.flags ? cli.flags.profile : guessProfile(term);
|
125 | const guess = {
|
126 | term,
|
127 | profile
|
128 | };
|
129 | if ("term" in cli.flags || "profile" in cli.flags) {
|
130 | const unsatisfied = ["term", "profile"].filter(n => !Boolean(guess[n]));
|
131 | if (unsatisfied.length > 0 && term !== "hyper") {
|
132 | throw error(`svg-term: --term and --profile must be used together, ${unsatisfied.join(", ")} missing`);
|
133 | }
|
134 | }
|
135 | const unknown = ensure(["term"], cli.flags, (name, val) => {
|
136 | if (!(name in cli.flags)) {
|
137 | return null;
|
138 | }
|
139 | if ((cli.flags.term in parsers.TermSchemes)) {
|
140 | return null;
|
141 | }
|
142 | return new TypeError(`${name} expected to be one of ${Object.keys(parsers.TermSchemes).join(", ")}, received "${val}"`);
|
143 | });
|
144 | if (unknown.length > 0) {
|
145 | throw error(`svg-term: ${unknown.map(m => m.message).join("\n")}`);
|
146 | }
|
147 | const p = guess.profile || "";
|
148 | const isFileProfile = ["~", "/", "."].indexOf(p.charAt(0)) > -1;
|
149 | if (isFileProfile && "profile" in cli.flags) {
|
150 | const missing = !fs.existsSync(path.join(process.cwd(), cli.flags.profile));
|
151 | if (missing) {
|
152 | throw error(`svg-term: ${cli.flags.profile} must be readable file but was not found`);
|
153 | }
|
154 | }
|
155 | const theme = getTheme(guess);
|
156 | const svg = render(input, {
|
157 | at: toNumber(cli.flags.at),
|
158 | cursor: toBoolean(cli.flags.cursor, true),
|
159 | from: toNumber(cli.flags.from),
|
160 | paddingX: toNumber(cli.flags.paddingX || cli.flags.padding),
|
161 | paddingY: toNumber(cli.flags.paddingY || cli.flags.padding),
|
162 | to: toNumber(cli.flags.to),
|
163 | height: toNumber(cli.flags.height),
|
164 | theme,
|
165 | width: toNumber(cli.flags.width),
|
166 | window: toBoolean(cli.flags.window, false)
|
167 | });
|
168 | const svgo = new SVGO({
|
169 | plugins: [{ collapseGroups: false }]
|
170 | });
|
171 | const optimized = toBoolean(cli.flags.optimize, true)
|
172 | ? yield svgo.optimize(svg)
|
173 | : { data: svg };
|
174 | if (typeof cli.flags.out === "string") {
|
175 | sander.writeFile(cli.flags.out, Buffer.from(optimized.data));
|
176 | }
|
177 | else {
|
178 | process.stdout.write(optimized.data);
|
179 | }
|
180 | });
|
181 | }
|
182 | function command(name) {
|
183 | return __awaiter(this, void 0, void 0, function* () {
|
184 | try {
|
185 | return (yield commandExists(name)) === name;
|
186 | }
|
187 | catch (err) {
|
188 | return false;
|
189 | }
|
190 | });
|
191 | }
|
192 | function ensure(names, flags, predicate) {
|
193 | return names
|
194 | .map(name => predicate(name, flags[name]))
|
195 | .filter(e => e instanceof Error)
|
196 | .map(e => e);
|
197 | }
|
198 | function cliError(cli) {
|
199 | return message => {
|
200 | const err = new Error(message);
|
201 | err.help = () => cli.help;
|
202 | return err;
|
203 | };
|
204 | }
|
205 | function getConfig(term) {
|
206 | switch (term) {
|
207 | case guess_terminal_1.GuessedTerminal.terminal: {
|
208 | return macosAppConfig.sync(term)[0];
|
209 | }
|
210 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
211 | return macosAppConfig.sync(term)[0];
|
212 | }
|
213 | default:
|
214 | return null;
|
215 | }
|
216 | }
|
217 | function getPresets(term) {
|
218 | const config = getConfig(term);
|
219 | switch (term) {
|
220 | case guess_terminal_1.GuessedTerminal.terminal: {
|
221 | return config["Window Settings"];
|
222 | }
|
223 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
224 | return config["Custom Color Presets"];
|
225 | }
|
226 | default:
|
227 | return null;
|
228 | }
|
229 | }
|
230 | function guessProfile(term) {
|
231 | if (os.platform() !== "darwin") {
|
232 | return null;
|
233 | }
|
234 | const config = getConfig(term);
|
235 | if (!config) {
|
236 | return null;
|
237 | }
|
238 | switch (term) {
|
239 | case guess_terminal_1.GuessedTerminal.terminal: {
|
240 | return config["Default Window Settings"];
|
241 | }
|
242 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
243 | return null;
|
244 | }
|
245 | default:
|
246 | return null;
|
247 | }
|
248 | }
|
249 | function getInput(cli) {
|
250 | return __awaiter(this, void 0, void 0, function* () {
|
251 | if (cli.flags.command) {
|
252 | return record(cli.flags.command);
|
253 | }
|
254 | if (cli.flags.in) {
|
255 | return String(yield sander.readFile(cli.flags.in));
|
256 | }
|
257 | if (cli.flags.cast) {
|
258 | const response = yield fetch(`https://asciinema.org/a/${cli.flags.cast}.cast?dl=true`);
|
259 | return response.text();
|
260 | }
|
261 | return getStdin();
|
262 | });
|
263 | }
|
264 | function getParser(term) {
|
265 | switch (term) {
|
266 | case parsers.TermSchemes.iterm2:
|
267 | return parsers.iterm2;
|
268 | case parsers.TermSchemes.konsole:
|
269 | return parsers.konsole;
|
270 | case parsers.TermSchemes.remmina:
|
271 | return parsers.remmina;
|
272 | case parsers.TermSchemes.terminal:
|
273 | return parsers.terminal;
|
274 | case parsers.TermSchemes.terminator:
|
275 | return parsers.terminator;
|
276 | case parsers.TermSchemes.termite:
|
277 | return parsers.termite;
|
278 | case parsers.TermSchemes.tilda:
|
279 | return parsers.tilda;
|
280 | case parsers.TermSchemes.xcfe:
|
281 | return parsers.xfce;
|
282 | case parsers.TermSchemes.xresources:
|
283 | return parsers.xresources;
|
284 | case parsers.TermSchemes.xterm:
|
285 | return parsers.xterm;
|
286 | default:
|
287 | throw new Error(`unknown term parser: ${term}`);
|
288 | }
|
289 | }
|
290 | function getTheme(guess) {
|
291 | if (guess.term === null && guess.profile === null) {
|
292 | return null;
|
293 | }
|
294 | const p = guess.profile || "";
|
295 | const isFileProfile = ["~", "/", "."].indexOf(p.charAt(0)) > -1;
|
296 | return isFileProfile
|
297 | ? parseTheme(guess.term, guess.profile)
|
298 | : extractTheme(guess.term, guess.profile);
|
299 | }
|
300 | function parseTheme(term, input) {
|
301 | const parser = getParser(term);
|
302 | return parser(String(fs.readFileSync(input)));
|
303 | }
|
304 | function extractTheme(term, name) {
|
305 | if (!(term in guess_terminal_1.GuessedTerminal)) {
|
306 | return null;
|
307 | }
|
308 | if (os.platform() !== "darwin") {
|
309 | return null;
|
310 | }
|
311 | if (term === guess_terminal_1.GuessedTerminal.hyper) {
|
312 | const filename = path.resolve(os.homedir(), ".hyper.js");
|
313 | return parsers.hyper(String(fs.readFileSync(filename)), {
|
314 | filename
|
315 | });
|
316 | }
|
317 | const presets = getPresets(term);
|
318 | if (!presets) {
|
319 | return null;
|
320 | }
|
321 | if (!(name in presets)) {
|
322 | throw new Error(`profile "${name}" not found for terminal "${term}". Available: ${Object.keys(presets).join(', ')}`);
|
323 | }
|
324 | const theme = presets[name];
|
325 | const parser = getParser(term);
|
326 | if (!theme) {
|
327 | return null;
|
328 | }
|
329 | switch (term) {
|
330 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
331 | return parser(plist.build(theme));
|
332 | }
|
333 | case guess_terminal_1.GuessedTerminal.terminal:
|
334 | return parser(plist.build(theme));
|
335 | default:
|
336 | return null;
|
337 | }
|
338 | }
|
339 | function record(cmd, options = {}) {
|
340 | return __awaiter(this, void 0, void 0, function* () {
|
341 | const tmp = tempy.file({ extension: '.json' });
|
342 | const result = yield execa('asciinema', [
|
343 | 'rec',
|
344 | '-c', cmd,
|
345 | ...(options.title ? ['-t', options.title] : []),
|
346 | tmp
|
347 | ]);
|
348 | if (result.code > 0) {
|
349 | throw new Error(`recording "${cmd}" failed\n${result.stdout}\n${result.stderr}`);
|
350 | }
|
351 | return String(yield sander.readFile(tmp));
|
352 | });
|
353 | }
|
354 | function toNumber(input) {
|
355 | if (!input) {
|
356 | return null;
|
357 | }
|
358 | const candidate = parseInt(input, 10);
|
359 | if (isNaN(candidate)) {
|
360 | return null;
|
361 | }
|
362 | return candidate;
|
363 | }
|
364 | function toBoolean(input, fb) {
|
365 | if (input === undefined) {
|
366 | return fb;
|
367 | }
|
368 | if (input === "false") {
|
369 | return false;
|
370 | }
|
371 | if (input === "true") {
|
372 | return true;
|
373 | }
|
374 | return input === true;
|
375 | }
|
376 | function withCli(fn, help = "", options = {}) {
|
377 | const unknown = [];
|
378 | const cli = meow(help, Object.assign({}, options, { unknown: (arg) => {
|
379 | unknown.push(arg);
|
380 | return false;
|
381 | } }));
|
382 | if (unknown.length > 0) {
|
383 | console.log(cli.help);
|
384 | console.log("\n", chalk_1.default.red(`svg-term: remove unknown flags ${unknown.join(', ')}`));
|
385 | process.exit(1);
|
386 | }
|
387 | fn(cli).catch(err => {
|
388 | console.log({ err });
|
389 | setTimeout(() => {
|
390 | if (typeof err.help === "function") {
|
391 | console.log(err.help());
|
392 | console.log("\n", chalk_1.default.red(err.message));
|
393 | process.exit(1);
|
394 | }
|
395 | throw err;
|
396 | }, 0);
|
397 | });
|
398 | }
|