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 os = require("os");
|
14 | const path = require("path");
|
15 | const guess_terminal_1 = require("guess-terminal");
|
16 | const macosAppConfig = require("macos-app-config");
|
17 | const meow = require("meow");
|
18 | const parsers = require("term-schemes");
|
19 | const term_schemes_1 = require("term-schemes");
|
20 | const plist = require('plist');
|
21 | const fetch = require('node-fetch');
|
22 | const getStdin = require('get-stdin');
|
23 | const { render } = require('svg-term');
|
24 | const sander = require('@marionebl/sander');
|
25 | withCli(main, `
|
26 | Usage
|
27 | $ svg-term [options]
|
28 |
|
29 | Options
|
30 | --at timestamp of frame to render in ms [number]
|
31 | --cast asciinema cast id to download [string], required if no stdin provided
|
32 | --cursor display cursor, defaults to true [boolean]
|
33 | --frame wether to frame the result with an application window [boolean]
|
34 | --from lower range of timeline to render in ms [number]
|
35 | --height height in lines [number]
|
36 | --help print this help [boolean]
|
37 | --out output file, emits to stdout if omitted
|
38 | --padding distance between text and image bounds
|
39 | --padding-x distance between text and image bounds on x axis
|
40 | --padding-y distance between text and image bounds on y axis
|
41 | --profile terminal profile file to use [file], requires --term
|
42 | --term terminal profile format, requires [iterm2, xrdb, xresources, terminator, konsole, terminal, remmina, termite, tilda, xcfe] --profile
|
43 | --to upper range of timeline to render in ms [number]
|
44 | --width width in columns [number]
|
45 |
|
46 | Examples
|
47 | $ echo rec.json | svg-term
|
48 | $ svg-term --cast 113643
|
49 | $ svg-term --cast 113643 --out examples/parrot.svg
|
50 | `);
|
51 | function main(cli) {
|
52 | return __awaiter(this, void 0, void 0, function* () {
|
53 | const input = yield getInput(cli);
|
54 | const error = cliError(cli);
|
55 | if (!input) {
|
56 | throw error(`svg-term: either stdin or --cast are required`);
|
57 | }
|
58 | const malformed = ensure(['height', 'width'], cli.flags, (name, val) => {
|
59 | if (!(name in cli.flags)) {
|
60 | return;
|
61 | }
|
62 | const candidate = parseInt(val, 10);
|
63 | if (isNaN(candidate)) {
|
64 | return new TypeError(`${name} expected to be number, received "${val}"`);
|
65 | }
|
66 | });
|
67 | if (malformed.length > 0) {
|
68 | throw error(`svg-term: ${malformed.map(m => m.message).join('\n')}`);
|
69 | }
|
70 | const missingValue = ensure(['cast', 'out', 'profile'], cli.flags, (name, val) => {
|
71 | if (!(name in cli.flags)) {
|
72 | return;
|
73 | }
|
74 | if (name === 'cast' && typeof val === 'number') {
|
75 | return;
|
76 | }
|
77 | if (typeof val !== 'string') {
|
78 | return new TypeError(`${name} expected to be string, received "${val}"`);
|
79 | }
|
80 | });
|
81 | if (missingValue.length > 0) {
|
82 | throw error(`svg-term: ${missingValue.map(m => m.message).join('\n')}`);
|
83 | }
|
84 | const shadowed = ensure(['at', 'from', 'to'], cli.flags, (name, val) => {
|
85 | if (!(name in cli.flags)) {
|
86 | return;
|
87 | }
|
88 | if (typeof val !== 'number' || isNaN(val)) {
|
89 | return new TypeError(`${name} expected to be number, received "${val}"`);
|
90 | }
|
91 | if (name !== 'at' && typeof cli.flags.at === 'number') {
|
92 | return new TypeError(`--at flag disallows --${name}`);
|
93 | }
|
94 | });
|
95 | if (shadowed.length > 0) {
|
96 | throw error(`svg-term: ${shadowed.map(m => m.message).join('\n')}`);
|
97 | }
|
98 | const term = guess_terminal_1.guessTerminal() || cli.flags.term;
|
99 | const profile = term ? guessProfile(term) || cli.flags.profile : cli.flags.profile;
|
100 | const guess = {
|
101 | term,
|
102 | profile
|
103 | };
|
104 | if (('term' in cli.flags) || ('profile' in cli.flags)) {
|
105 | const unsatisfied = ['term', 'profile'].filter(n => !Boolean(guess[n]));
|
106 | if (unsatisfied.length > 0) {
|
107 | throw error(`svg-term: --term and --profile must be used together, ${unsatisfied.join(', ')} missing`);
|
108 | }
|
109 | }
|
110 | const unknown = ensure(['term'], cli.flags, (name, val) => {
|
111 | if (!(name in cli.flags)) {
|
112 | return;
|
113 | }
|
114 | if (!(cli.flags.term in term_schemes_1.TermSchemes)) {
|
115 | return new TypeError(`${name} expected to be one of ${Object.keys(term_schemes_1.TermSchemes).join(', ')}, received "${val}"`);
|
116 | }
|
117 | });
|
118 | if (unknown.length > 0) {
|
119 | throw error(`svg-term: ${unknown.map(m => m.message).join('\n')}`);
|
120 | }
|
121 | const p = guess.profile || '';
|
122 | const isFileProfile = ['~', '/', '.'].indexOf(p.charAt(0)) > -1;
|
123 | if (isFileProfile && 'profile' in cli.flags) {
|
124 | const missing = !fs.existsSync(path.join(process.cwd(), cli.flags.profile));
|
125 | if (missing) {
|
126 | throw error(`svg-term: ${cli.flags.profile} must be readable file but was not found`);
|
127 | }
|
128 | }
|
129 | const theme = getTheme(guess);
|
130 | const svg = render(input, {
|
131 | at: toNumber(cli.flags.at),
|
132 | cursor: toBoolean(cli.flags.cursor, true),
|
133 | from: toNumber(cli.flags.from),
|
134 | paddingX: toNumber(cli.flags.paddingX || cli.flags.padding),
|
135 | paddingY: toNumber(cli.flags.paddingY || cli.flags.padding),
|
136 | to: toNumber(cli.flags.to),
|
137 | height: toNumber(cli.flags.height),
|
138 | theme,
|
139 | width: toNumber(cli.flags.width),
|
140 | window: toBoolean(cli.flags.frame, false)
|
141 | });
|
142 | if (typeof cli.flags.out === 'string') {
|
143 | sander.writeFile(cli.flags.out, Buffer.from(svg));
|
144 | }
|
145 | else {
|
146 | process.stdout.write(svg);
|
147 | }
|
148 | });
|
149 | }
|
150 | function ensure(names, flags, predicate) {
|
151 | return names.map(name => predicate(name, flags[name])).filter(e => e instanceof Error);
|
152 | }
|
153 | function cliError(cli) {
|
154 | return (message) => {
|
155 | const err = new Error(message);
|
156 | err.help = () => cli.help;
|
157 | return err;
|
158 | };
|
159 | }
|
160 | function getConfig(term) {
|
161 | switch (term) {
|
162 | case guess_terminal_1.GuessedTerminal.terminal: {
|
163 | return macosAppConfig.sync(term)[0];
|
164 | }
|
165 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
166 | return macosAppConfig.sync(term)[0];
|
167 | }
|
168 | default:
|
169 | return null;
|
170 | }
|
171 | }
|
172 | function getPresets(term) {
|
173 | const config = getConfig(term);
|
174 | switch (term) {
|
175 | case guess_terminal_1.GuessedTerminal.terminal: {
|
176 | return config['Window Settings'];
|
177 | }
|
178 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
179 | return config['Custom Color Presets'];
|
180 | }
|
181 | default:
|
182 | return null;
|
183 | }
|
184 | }
|
185 | function guessProfile(term) {
|
186 | if (os.platform() !== 'darwin') {
|
187 | return null;
|
188 | }
|
189 | const config = getConfig(term);
|
190 | if (!config) {
|
191 | return null;
|
192 | }
|
193 | switch (term) {
|
194 | case guess_terminal_1.GuessedTerminal.terminal: {
|
195 | return config['Default Window Settings'];
|
196 | }
|
197 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
198 | const presets = config['Custom Color Presets'];
|
199 | }
|
200 | default:
|
201 | return null;
|
202 | }
|
203 | }
|
204 | function getInput(cli) {
|
205 | return __awaiter(this, void 0, void 0, function* () {
|
206 | if (cli.flags.cast) {
|
207 | const response = yield fetch(`https://asciinema.org/a/${cli.flags.cast}.cast?dl=true`);
|
208 | return response.text();
|
209 | }
|
210 | return yield getStdin();
|
211 | });
|
212 | }
|
213 | function getParser(term) {
|
214 | switch (term) {
|
215 | case term_schemes_1.TermSchemes.iterm2:
|
216 | return parsers.iterm2;
|
217 | case term_schemes_1.TermSchemes.konsole:
|
218 | return parsers.konsole;
|
219 | case term_schemes_1.TermSchemes.remmina:
|
220 | return parsers.remmina;
|
221 | case term_schemes_1.TermSchemes.terminal:
|
222 | return parsers.terminal;
|
223 | case term_schemes_1.TermSchemes.terminator:
|
224 | return parsers.terminator;
|
225 | case term_schemes_1.TermSchemes.termite:
|
226 | return parsers.termite;
|
227 | case term_schemes_1.TermSchemes.tilda:
|
228 | return parsers.tilda;
|
229 | case term_schemes_1.TermSchemes.xcfe:
|
230 | return parsers.xfce;
|
231 | case term_schemes_1.TermSchemes.xresources:
|
232 | return parsers.xresources;
|
233 | case term_schemes_1.TermSchemes.xterm:
|
234 | return parsers.xterm;
|
235 | default:
|
236 | throw new Error(`unknown term parser: ${term}`);
|
237 | }
|
238 | }
|
239 | function getTheme(guess) {
|
240 | if (guess.term === null || guess.profile === null) {
|
241 | return null;
|
242 | }
|
243 | const p = guess.profile || '';
|
244 | const isFileProfile = ['~', '/', '.'].indexOf(p.charAt(0)) > -1;
|
245 | return isFileProfile
|
246 | ? parseTheme(guess.term, guess.profile)
|
247 | : extractTheme(guess.term, guess.profile);
|
248 | }
|
249 | function parseTheme(term, input) {
|
250 | const parser = getParser(term);
|
251 | return parser(String(fs.readFileSync(input)));
|
252 | }
|
253 | function extractTheme(term, name) {
|
254 | if (!(term in guess_terminal_1.GuessedTerminal)) {
|
255 | return null;
|
256 | }
|
257 | if (os.platform() !== 'darwin') {
|
258 | return null;
|
259 | }
|
260 | if (term === guess_terminal_1.GuessedTerminal.hyper) {
|
261 | const filename = path.resolve(os.homedir(), '.hyper.js');
|
262 | const theme = parsers.hyper(String(fs.readFileSync(filename)), { filename });
|
263 | return theme;
|
264 | }
|
265 | const presets = getPresets(term);
|
266 | if (!presets) {
|
267 | return null;
|
268 | }
|
269 | const theme = presets[name];
|
270 | const parser = getParser(term);
|
271 | if (!theme) {
|
272 | return null;
|
273 | }
|
274 | switch (term) {
|
275 | case guess_terminal_1.GuessedTerminal.iterm2: {
|
276 | return parser(plist.build(theme));
|
277 | }
|
278 | case guess_terminal_1.GuessedTerminal.terminal:
|
279 | return parser(plist.build(theme));
|
280 | default:
|
281 | return null;
|
282 | }
|
283 | }
|
284 | function toNumber(input) {
|
285 | if (!input) {
|
286 | return null;
|
287 | }
|
288 | const candidate = parseInt(input, 10);
|
289 | if (isNaN(candidate)) {
|
290 | return null;
|
291 | }
|
292 | return candidate;
|
293 | }
|
294 | function toBoolean(input, fb) {
|
295 | if (typeof input === 'undefined') {
|
296 | return fb;
|
297 | }
|
298 | if (input === 'false') {
|
299 | return false;
|
300 | }
|
301 | if (input === 'true') {
|
302 | return true;
|
303 | }
|
304 | return input === true;
|
305 | }
|
306 | function withCli(fn, help = '') {
|
307 | return main(meow(help))
|
308 | .catch(err => {
|
309 | setTimeout(() => {
|
310 | if (typeof err.help === 'function') {
|
311 | console.log(err.help());
|
312 | console.log('\n', err.message);
|
313 | process.exit(1);
|
314 | }
|
315 | throw err;
|
316 | }, 0);
|
317 | });
|
318 | }
|