UNPKG

14.4 kBJavaScriptView Raw
1#!/usr/bin/env node
2"use strict";
3var __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};
11Object.defineProperty(exports, "__esModule", { value: true });
12const fs = require("fs");
13const chalk_1 = require("chalk");
14const execa = require("execa");
15const guess_terminal_1 = require("guess-terminal");
16const macosAppConfig = require("macos-app-config");
17const os = require("os");
18const path = require("path");
19const tempy = require("tempy");
20const parsers = require("term-schemes");
21const commandExists = require("command-exists");
22const meow = require("meow");
23const plist = require("plist");
24const fetch = require("node-fetch");
25const getStdin = require("get-stdin");
26const { render } = require("svg-term");
27const sander = require("@marionebl/sander");
28const SVGO = require("svgo");
29withCli(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});
66function 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}
182function 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}
192function 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}
198function cliError(cli) {
199 return message => {
200 const err = new Error(message);
201 err.help = () => cli.help;
202 return err;
203 };
204}
205function 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}
217function 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}
230function 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}
249function 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}
264function 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}
290function 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}
300function parseTheme(term, input) {
301 const parser = getParser(term);
302 return parser(String(fs.readFileSync(input)));
303}
304function 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}
339function 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}
354function 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}
364function 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}
376function 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}