├── .gitignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── appveyor.yml
├── examples
├── chest.svg
├── jsvu.svg
├── nyan.svg
└── parrot.svg
├── package.json
├── src
├── cli.test.ts
└── cli.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | *.tgz
4 | *.log
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | - '6'
5 | cache: yarn
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | Copyright 2018 - present Mario Nebl
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > Share terminal sessions as razor-sharp animated SVG everywhere
2 |
3 |
4 |
5 |
6 |
7 | > Example generated with `svg-term --cast 113643 --out examples/parrot.svg --window --no-cursor --from=4500`
8 |
9 | # svg-term-cli
10 |
11 | * 💄 Render asciicast to animated SVG
12 | * 🌐 Share asciicasts everywhere (sans JS)
13 | * 🤖 Style with common [color profiles](https://github.com/marionebl/term-schemes#supported-formats)
14 |
15 | ## Install
16 |
17 | 1. Install asciinema via: https://asciinema.org/docs/installation
18 | 2. Install svg-term-cli:
19 | ```sh
20 | npm install -g svg-term-cli
21 | ```
22 |
23 | ## Usage
24 |
25 | Generate the `parrot.svg` example from asciicast at
26 |
27 | ```
28 | svg-term --cast=113643 --out examples/parrot.svg --window
29 | ```
30 |
31 | ## Interface
32 |
33 | ```
34 | λ svg-term --help
35 |
36 | Share terminal sessions as razor-sharp animated SVG everywhere
37 |
38 | Usage
39 | $ svg-term [options]
40 |
41 | Options
42 | --at timestamp of frame to render in ms [number]
43 | --cast asciinema cast id to download [string], required if no stdin provided [string]
44 | --command command to record [string]
45 | --from lower range of timeline to render in ms [number]
46 | --height height in lines [number]
47 | --help print this help [boolean]
48 | --in json file to use as input [string]
49 | --no-cursor disable cursor rendering [boolean]
50 | --no-optimize disable svgo optimization [boolean]
51 | --out output file, emits to stdout if omitted, [string]
52 | --padding distance between text and image bounds, [number]
53 | --padding-x distance between text and image bounds on x axis [number]
54 | --padding-y distance between text and image bounds on y axis [number]
55 | --profile terminal profile file to use, requires --term [string]
56 | --term terminal profile format [iterm2, xrdb, xresources, terminator, konsole, terminal, remmina, termite, tilda, xcfe], requires --profile [string]
57 | --to upper range of timeline to render in ms [number]
58 | --width width in columns [number]
59 | --window render with window decorations [boolean]
60 |
61 | Examples
62 | $ cat rec.json | svg-term
63 | $ svg-term --cast 113643
64 | $ svg-term --cast 113643 --out examples/parrot.svg
65 | ```
66 |
67 | ## Rationale
68 |
69 | Replace GIF asciicast recordings where you can not use the [asciinema player](https://asciinema.org/), e.g. `README.md` files on GitHub and the npm registry.
70 |
71 | The image at the top of this README is an example. See how sharp the text looks, even when you zoom in? That’s because it’s an SVG!
72 |
73 | ## Related
74 |
75 | * [asciinema/asciinema](https://github.com/asciinema/asciinema) - Terminal session recorder
76 | * [derhuerst/asciicast-to-svg](https://github.com/derhuerst/asciicast-to-svg) - Render frames of Asciicasts as SVGs
77 | * [marionebl/svg-term](https://github.com/marionebl/svg-term) - Render asciicast to animated SVG
78 | * [marionebl/term-schemes](https://github.com/marionebl/term-schemes) - Parse and normalize common terminal emulator color schemes
79 |
80 | ## Gallery
81 |
82 | * [marionebl/commitlint](https://github.com/marionebl/commitlint)
83 | * [marionebl/share-cli](https://github.com/marionebl/share-cli)
84 | * [marionebl/remote-share-cli](https://github.com/marionebl/remote-share-cli)
85 |
86 | ## License
87 |
88 | Copyright 2017. Released under the MIT license.
89 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | matrix:
3 | - nodejs_version: '6'
4 | install:
5 | - ps: Install-Product node $env:nodejs_version
6 | - set CI=true
7 | - yarn
8 | matrix:
9 | fast_finish: true
10 | build: off
11 | version: '{build}'
12 | test_script:
13 | - yarn test
--------------------------------------------------------------------------------
/examples/jsvu.svg:
--------------------------------------------------------------------------------
1 | mathias at mathBook in /jsvu-demo ❯ Extracting… ✔ Extraction completed. ✔ Testing completed. bash: v8: command not found $ export PATH="${HOME}/.jsvu:${PATH}" # see https://mths.be/jsvu#installation $ jsvu 📦 jsvu v1.0.0 — the J ava S cript engine V ersion U pdater 📦 ? What is your operating system? macOS 64-bit ? Which JavaScript engines would you like to install? Chakra/ChakraCore, JavaScriptCore, SpiderMonkey, V8 ✔ Found latest Chakra version: v1.7.4. ✔ URL: https://aka.ms/chakracore/cc_osx_x64_1_7_4 ✔ SHA-256 checksum: 71473323193c6285f268160e2ec5bf8e14d6b6bc8c4844040b7b408d750b8a2a ✔ Download and checksum verification completed. Installing library to ~/.jsvu/engines/chakra/lib/libChakraCore.dylib… Installing binary to ~/.jsvu/engines/chakra/chakra… Installing symlink at ~/.jsvu/chakra pointing to ~/.jsvu/engines/chakra/chakra… Installing symlink at ~/.jsvu/ch pointing to ~/.jsvu/engines/chakra/chakra… Installing license to ~/.jsvu/engines/chakra/LICENSE-chakra… ✔ Chakra v1.7.4 has been installed! 🎉 ✔ Found latest JavaScriptCore version: v225521. ✔ URL: https://s3-us-west-2.amazonaws.com/minified-archives.webkit.org/mac-highsierra-x86_64-release/225521.zip ✔ Download completed. Installing library to ~/.jsvu/engines/javascriptcore/JavaScriptCore.framework/Headers… Installing library to ~/.jsvu/engines/javascriptcore/JavaScriptCore.framework/JavaScriptCore… Installing library to ~/.jsvu/engines/javascriptcore/JavaScriptCore.framework/PrivateHeaders… Installing library to ~/.jsvu/engines/javascriptcore/JavaScriptCore.framework/Resources… Installing library to ~/.jsvu/engines/javascriptcore/JavaScriptCore.framework/Versions… Installing binary to ~/.jsvu/engines/javascriptcore/javascriptcore… Installing wrapper script to ~/.jsvu/javascriptcore… Installing symlink at ~/.jsvu/jsc pointing to ~/.jsvu/javascriptcore… ✔ JavaScriptCore v225521 has been installed! 🎉 ✔ Found latest SpiderMonkey version: v58.0b9.
--------------------------------------------------------------------------------
/examples/parrot.svg:
--------------------------------------------------------------------------------
1 | .ccccccc. .ccckNKOOOOkdcc. .;;cc:ccccccc:,::::,,. .c;:;.,cccllxOOOxlllc,;ol. .lkc,coxo:;oOOxooooooo;..:, .cdc.,dOOOc..cOd,.',,;'....':c. cNx'.lOOOOxlldOl..;lll;.....cO; ,do;,:dOOOOOOOOOl'':lll;..:d:.'c, co..lOOOOOOOOOOOl'':lll;.'lOd,.cd. co.,dOOOOOOOOOOOo,.;llc,.,dOOc..dc co..lOOOOOOOOOOOOc.';:,..cOOOl..oc .,:;.'::lxOOOOOOOOOo:'...,:oOOOc..dc ;Oc..cl'':llxOOOOOOOOdcclxOOOOx,.cd. .:;';lxl''''':lldOOOOOOOOOOOOOOc..oc .cccccccc. .,,,;;cc:cccccc:;;,. .cdxo;..,::cccc::,..;l. ,oo:,,:c:cdxxdllll:;,';:,. .cl;.,oxxc'.,cc,.',;;'...oNc ;Oc..cxxxc'.,c;..;lll;...cO; .;;',:ldxxxdoldxc..;lll:'...'c, ;c..cxxxxkxxkxxxc'.;lll:'','.cdc. .c;.;odxxxxxxxxxxxd;.,cll;.,l:.'dNc .:,''ccoxkxxkxxxxxxx:..,:;'.:xc..oNc .lc,.'lc':dxxxkxxxxxxxdl,...',lx:..dNc .:,',coxoc;;ccccoxxxxxxxxo:::oxxo,.cdc. .;':;.'oxxxxxc''''';cccoxxxxxxxxxkxc..oc .cccc;;cc;';c. .,:dkdc:;;:c:,:d:. .loc'.,cc::::::,..,:. .cl;....;dkdccc::,...c; .c:,';:'..ckc',;::;....;c. .c:'.,dkkoc:ok:;llllc,,c,';:. .;c,';okkkkkkkk:,lllll,:kd;.;:,. co..:kkkkkkkkkk:;llllc':kkc..oNc .cl;.,okkkkkkkkkkc,:cll;,okkc'.cO; ;k:..ckkkkkkkkkkkl..,;,.;xkko:',l' .,...';dkkkkkkkkkkd;.....ckkkl'.cO; .,,:,.;oo:ckkkkkkkkkkkdoc;;cdkkkc..cd, .cclo;,ccdkkl;llccdkkkkkkkkkkkkkkkd,.c; .lol:;;okkkkkxooc::loodkkkkkkkkkkkko'.oc .c:'..lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd,.oc .lo;,ccdkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd,.c; .ckx;'........':c. .,:c:c:::oxxocoo::::,',. .odc'..:lkkoolllllo;..;d, ;c..:o:..;:..',;'.......;. ,c..:0Xx::o:.,cllc:,'::,.,c. ;c;lkXXXXXXl.;lllll;lXXOo;':c. ,dc.oXXXXXXXXl.,lllll;lXXXXx,c0: ;Oc.oXXXXXXXXo.':ll:;'oXXXXO;,l' 'l;;OXXXXXXXXd'.'::'..dXXXXO;,l' 'l;:0XXXXXXXX0x:...,:o0XXXXk,:x, 'l;;kXXXXXXKXXXkol;oXXXXXXXO;oNc ,c'..ckk0XXXXXXXXXX00XXXXXXX0:;o:. .':;..:dd::ooooOXXXXXXXXXXXXXXXo..c; .',',:co0XX0kkkxx0XXXXXXXXXXXXXXX0c..;l. .:;'..oXXXXXXXXXXXXXXXXXXXXXXXXXXXXXko;';:. .cdc..:oOXXXXXXXXKXXXXXXXXXXXXXXXXXXXXXXo..oc .cc;. ... .;c. .,,cc:cc:lxxxl:ccc:;,. .lo;...lKKklllookl..cO; .cl;.,;'.okl;...'.;,..';:. .:o;;dkx,.ll..,cc::,..,'.;:,. co..lKKKkokl.':lllo;''ol..;dl. .,c;.,xKKKKKKo.':llll;.'oOxo,.cl,. cNo..lKKKKKKKo'';llll;;okKKKl..oNc cNo..lKKKKKKKko;':c:,'lKKKKKo'.oNc cNo..lKKKKKKKKKl.....'dKKKKKxc,l0: .c:'.lKKKKKKKKKk;....oKKKKKKo'.oNc ,:.,oxOKKKKKKKOxxxxOKKKKKKxc,;ol:. ;c..'':oookKKKKKKKKKKKKKKKKKk:.'clc. ,dl'.,oxo;'';oxOKKKKKKKKKKKKKKKOxxl::;,,. .dOc..lKKKkoooookKKKKKKKKKKKKKKKKKKKxl,;ol. cx,';okKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKl..;lc. .ccccccc. .,,,;cooolccol;;,,. .dOx;..;lllll;..;xOd. .cdo,',loOXXXXXkll;';odc. ,oo:;c,':oko:cccccc,...ckl. ;c.;kXo..::..;c::'.......oc ,dc..oXX0kk0o.':lll;..cxxc.,ld, kNo.'oXXXXXXo'':lll;..oXXOd;cOd. KOc;oOXXXXXXo.':lol,..dXXXXl';xc Ol,:k0XXXXXX0c.,clc'.:0XXXXx,.oc KOc;dOXXXXXXXl..';'..lXXXXXd..oc dNo..oXXXXXXXOx:..'lxOXXXXXk,.:; .. cNo..lXXXXXXXXXOolkXXXXXXXXXkl;..;:.;. .,;'.,dkkkkk0XXXXXXXXXXXXXXXXXOxxl;,;,;l:. ;c.;:''''':doOXXXXXXXXXXXXXXXXXXOdo;';clc. ;c.lOdood:'''oXXXXXXXXXXXXXXXXXXXXXk,..;ol. .;:;;,.,;;::,. .;':;........'co:. .clc;'':cllllc::,.':c. .lo;;o:coxdlooollc;',::,,. .c:'.,cl,.'lc',,;;'......cO; do;';oxoc::l;;llllc'.';;'.';. c..ckkkkkkkd,;llllc'.:kkd;.':c. '.,okkkkkkkkc;llllc,.:kkkdl,cO; ..;xkkkkkkkkc,ccll:,;okkkkk:,cl, ..,dkkkkkkkkc..,;,'ckkkkkkkc;ll. ..'okkkkkkkko,....'okkkkkkkc,:c. c..ckkkkkkkkkdl;,:okkkkkkkkd,.',';. d..':lxkkkkkkkkxxkkkkkkkkkkkdoc;,;'..'.,. o...'';llllldkkkkkkkkkkkkkkkkkkdll;..'cdo. o..,l;'''''';dkkkkkkkkkkkkkkkkkkkkdlc,..;lc. .,,,,,,,,,. .ckKxodooxOOdcc. .cclooc'....';;cool. .loc;;;;clllllc;;;;;:;,. .c:'.,okd;;cdo:::::cl,..oc .:o;';okkx;';;,';::;'....,;,. co..ckkkkkddk:,cclll;.,c:,:o:. co..ckkkkkkkk:,cllll;.:kkd,.':c. .,:;.,okkkkkkkk:,cclll;.:kkkdl;;o:. cNo..ckkkkkkkkko,.;llc,.ckkkkkc..oc ,dd;.:kkkkkkkkkx;..;:,.'lkkkkko,.:, ;c.ckkkkkkkkkkc.....;ldkkkkkk:.,' ,dc..'okkkkkkkkkxoc;;cxkkkkkkkkc..,;,. kNo..':lllllldkkkkkkkkkkkkkkkkkdcc,.;l. KOc,l;''''''';lldkkkkkkkkkkkkkkkkkc..;lc. .,,,,,,,,,. .ckKxodooxOOdcc. .cclooc'....';;cool. .loc;;;;clllllc;;;;;:;,. .c:'.,okd;;cdo:::::cl,..oc .:o;';okkx;';;,';::;'....,;,. co..ckkkkkddk:,cclll;.,c:,:o:. co..ckkkkkkkk:,cllll;.:kkd,.':c. .,:;.,okkkkkkkk:,cclll;.:kkkdl;;o:. cNo..ckkkkkkkkko,.;llc,.ckkkkkc..oc ,dd;.:kkkkkkkkkx;..;:,.'lkkkkko,.:, ;c.ckkkkkkkkkkc.....;ldkkkkkk:.,' ,dc..'okkkkkkkkkxoc;;cxkkkkkkkkc..,;,. kNo..':lllllldkkkkkkkkkkkkkkkkkdcc,.;l. KOc,l;''''''';lldkkkkkkkkkkkkkkkkkc..;lc. ✔ lucy@koharu ~ 01:01 $ parrot -loops 10 && exit
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svg-term-cli",
3 | "version": "2.1.1",
4 | "description": "Share terminal sessions as razor-sharp animated SVG everywhere",
5 | "bin": {
6 | "svg-term": "./lib/cli.js"
7 | },
8 | "files": [
9 | "lib"
10 | ],
11 | "scripts": {
12 | "build": "tsc",
13 | "format": "prettier src/**/*.ts --write && tslint --project . --fix",
14 | "test": "jest"
15 | },
16 | "jest": {
17 | "globals": {
18 | "ts-jest": {
19 | "skipBabel": true
20 | }
21 | },
22 | "moduleFileExtensions": [
23 | "ts",
24 | "js",
25 | "json"
26 | ],
27 | "transform": {
28 | "^.+\\.tsx?$": "ts-jest"
29 | },
30 | "testRegex": ".*\\.test.ts"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/marionebl/svg-term-cli.git"
35 | },
36 | "keywords": [
37 | "svg",
38 | "asciinema",
39 | "asciicast"
40 | ],
41 | "author": "Mario Nebl ",
42 | "license": "MIT",
43 | "bugs": {
44 | "url": "https://github.com/marionebl/svg-term-cli/issues"
45 | },
46 | "homepage": "https://github.com/marionebl/svg-term-cli#readme",
47 | "dependencies": {
48 | "@marionebl/sander": "^0.6.1",
49 | "chalk": "^2.3.0",
50 | "command-exists": "^1.2.2",
51 | "execa": "^0.8.0",
52 | "get-stdin": "^5.0.1",
53 | "guess-terminal": "^1.0.0",
54 | "macos-app-config": "^1.0.1",
55 | "meow": "^3.7.0",
56 | "node-fetch": "^1.7.3",
57 | "plist": "^2.1.0",
58 | "svg-term": "^1.1.3",
59 | "svgo": "^1.0.3",
60 | "tempfile": "^2.0.0",
61 | "tempy": "^0.2.1",
62 | "term-schemes": "^1.2.0"
63 | },
64 | "devDependencies": {
65 | "@types/chalk": "^2.2.0",
66 | "@types/execa": "^0.8.0",
67 | "@types/jest": "^21.1.8",
68 | "@types/meow": "^3.6.2",
69 | "@types/svgo": "^0.7.0",
70 | "@types/tempfile": "^2.0.0",
71 | "@types/tempy": "^0.1.0",
72 | "jest": "^21.2.1",
73 | "jest-cli": "^21.2.1",
74 | "prettier": "^1.9.2",
75 | "ts-jest": "^21.2.3",
76 | "ts-node": "^3.3.0",
77 | "tslint": "^5.8.0",
78 | "tslint-config-prettier": "^1.6.0",
79 | "tslint-xo": "^0.4.0",
80 | "typescript": "^2.6.1",
81 | "xmldom": "^0.1.27"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/cli.test.ts:
--------------------------------------------------------------------------------
1 | import * as execa from "execa";
2 | import * as path from "path";
3 |
4 | const {DOMParser} = require('xmldom');
5 |
6 | const parser = new DOMParser();
7 | const pkg = require("../package");
8 |
9 | const bin = async (args: string[] = [], options = {}) => {
10 | try {
11 | return await execa(
12 | "ts-node",
13 | [path.join(__dirname, "./cli.ts"), ...args],
14 | options
15 | );
16 | } catch (err) {
17 | return err;
18 | }
19 | };
20 |
21 | test("prints help with non-zero exit code", async () => {
22 | const result = await bin([], { input: "" });
23 | expect(result.code).not.toBe(0);
24 | expect(result.stderr).toContain(
25 | "svg-term: either stdin, --cast, --command or --in are required"
26 | );
27 | });
28 |
29 | test("prints help with zero exit code for --help", async () => {
30 | const result = await bin(["--help"], { input: "" });
31 | expect(result.code).toBe(0);
32 | expect(result.stdout).toContain("print this help");
33 | });
34 |
35 | test("prints version with zero exit code for --version", async () => {
36 | const result = await bin(["--version"], { input: "" });
37 | expect(result.code).toBe(0);
38 | expect(result.stdout).toBe(pkg.version);
39 | });
40 |
41 | test("works for minimal stdin input", async () => {
42 | const result = await bin([], {
43 | input: '[{"version": 2, "width": 1, "height": 1}, [1, "o", "foo"]]'
44 | });
45 | expect(result.code).toBe(0);
46 | });
47 |
48 | test("is silent on stderr for minimal stdin input", async () => {
49 | const result = await bin([], {
50 | input: '[{"version": 2, "width": 1, "height": 1}, [1, "o", "foo"]]'
51 | });
52 | expect(result.stderr).toBe("");
53 | expect(result.code).toBe(0);
54 | });
55 |
56 | test("emits svg for minimal stdin input", async () => {
57 | const result = await bin([], {
58 | input: '[{"version": 2, "width": 1, "height": 1}, [1, "o", "foo"]]'
59 | });
60 |
61 | const doc = parser.parseFromString(result.stdout, 'image/svg+xml');
62 | expect(doc.documentElement.tagName).toBe('svg');
63 | });
64 |
65 | test("fails for faulty stdin input", async () => {
66 | const result = await bin([], {
67 | input: '{}'
68 | });
69 | expect(result.code).toBe(1);
70 | });
71 |
72 | test("emits error on stderr for faulty stdin input", async () => {
73 | const result = await bin([], {
74 | input: '{}'
75 | });
76 | expect(result.stderr).toContain("only asciicast v1 and v2 formats can be opened");
77 | });
78 |
79 | test("is silent on stdout for faulty stdin input", async () => {
80 | const result = await bin([], {
81 | input: '{}'
82 | });
83 | expect(result.stdout).toBe("");
84 | });
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import chalk from "chalk";
3 | import * as execa from "execa";
4 | import { GuessedTerminal, guessTerminal } from "guess-terminal";
5 | import * as macosAppConfig from "macos-app-config";
6 | import * as os from "os";
7 | import * as path from "path";
8 | import * as tempy from "tempy";
9 | import * as parsers from "term-schemes";
10 |
11 | const commandExists = require("command-exists");
12 | const meow = require("meow");
13 | const plist = require("plist");
14 | const fetch = require("node-fetch");
15 | const getStdin = require("get-stdin");
16 | const { render } = require("svg-term");
17 | const sander = require("@marionebl/sander");
18 | const SVGO = require("svgo");
19 |
20 | interface Guesses {
21 | [key: string]: string | null;
22 | }
23 |
24 | interface SvgTermCli {
25 | flags: { [name: string]: any };
26 | help: string;
27 | input: string[];
28 | }
29 |
30 | interface SvgTermError extends Error {
31 | help(): string;
32 | }
33 |
34 | interface RecordOptions {
35 | title?: string;
36 | }
37 |
38 | withCli(
39 | main,
40 | `
41 | Usage
42 | $ svg-term [options]
43 |
44 | Options
45 | --at timestamp of frame to render in ms [number]
46 | --cast asciinema cast id to download [string], required if no stdin provided [string]
47 | --command command to record [string]
48 | --from lower range of timeline to render in ms [number]
49 | --height height in lines [number]
50 | --help print this help [boolean]
51 | --in json file to use as input [string]
52 | --no-cursor disable cursor rendering [boolean]
53 | --no-optimize disable svgo optimization [boolean]
54 | --out output file, emits to stdout if omitted, [string]
55 | --padding distance between text and image bounds, [number]
56 | --padding-x distance between text and image bounds on x axis [number]
57 | --padding-y distance between text and image bounds on y axis [number]
58 | --profile terminal profile file to use, requires --term [string]
59 | --term terminal profile format [iterm2, xrdb, xresources, terminator, konsole, terminal, remmina, termite, tilda, xcfe], requires --profile [string]
60 | --to upper range of timeline to render in ms [number]
61 | --width width in columns [number]
62 | --window render with window decorations [boolean]
63 |
64 | Examples
65 | $ cat rec.json | svg-term
66 | $ svg-term --cast 113643
67 | $ svg-term --cast 113643 --out examples/parrot.svg
68 | `,
69 | {
70 | boolean: ["cursor", "help", "optimize", "version", "window"],
71 | string: [
72 | "at",
73 | "cast",
74 | "command",
75 | "from",
76 | "height",
77 | "in",
78 | "out",
79 | "padding",
80 | "padding-x",
81 | "padding-y",
82 | "profile",
83 | "term",
84 | "to",
85 | "width"
86 | ],
87 | default: {
88 | cursor: true,
89 | optimize: true,
90 | window: false
91 | }
92 | }
93 | );
94 |
95 | async function main(cli: SvgTermCli) {
96 | const error = cliError(cli);
97 |
98 | if (cli.flags.hasOwnProperty("command") && !await command("asciinema")) {
99 | throw error(
100 | [
101 | `svg-term: asciinema must be installed when --command is specified.`,
102 | ` See instructions at: https://asciinema.org/docs/installation`
103 | ].join("\n")
104 | );
105 | }
106 |
107 | const input = await getInput(cli);
108 |
109 | if (!input) {
110 | throw error(
111 | `svg-term: either stdin, --cast, --command or --in are required`
112 | );
113 | }
114 |
115 | const malformed = ensure(["height", "width"], cli.flags, (name, val) => {
116 | if (!cli.flags.hasOwnProperty(name)) {
117 | return null;
118 | }
119 |
120 | const candidate = parseInt(val, 10);
121 | if (isNaN(candidate)) {
122 | return new TypeError(`${name} expected to be number, received "${val}"`);
123 | }
124 |
125 | return null;
126 | });
127 |
128 | if (malformed.length > 0) {
129 | throw error(`svg-term: ${malformed.map(m => m.message).join("\n")}`);
130 | }
131 |
132 | const missingValue = ensure(
133 | ["cast", "out", "profile"],
134 | cli.flags,
135 | (name, val) => {
136 | if (!cli.flags.hasOwnProperty(name)) {
137 | return null;
138 | }
139 | if (name === "cast" && typeof val === "number") {
140 | return null;
141 | }
142 | if (typeof val === "string") {
143 | return null;
144 | }
145 |
146 | return new TypeError(`${name} expected to be string, received "${val}"`);
147 | }
148 | );
149 |
150 | if (missingValue.length > 0) {
151 | throw error(`svg-term: ${missingValue.map(m => m.message).join("\n")}`);
152 | }
153 |
154 | const shadowed = ensure(["at", "from", "to"], cli.flags, (name, val) => {
155 | if (!cli.flags.hasOwnProperty(name)) {
156 | return null;
157 | }
158 |
159 | const v = typeof val === "number" ? val : parseInt(val, 10);
160 |
161 | if (isNaN(v)) {
162 | return new TypeError(`${name} expected to be number, received "${val}"`);
163 | }
164 |
165 | if (name !== "at" && !isNaN(parseInt(cli.flags.at, 10))) {
166 | return new TypeError(`--at flag disallows --${name}`);
167 | }
168 |
169 | return null;
170 | });
171 |
172 | if (shadowed.length > 0) {
173 | throw error(`svg-term: ${shadowed.map(m => m.message).join("\n")}`);
174 | }
175 |
176 | const term = cli.flags.hasOwnProperty("term")
177 | ? cli.flags.term
178 | : guessTerminal();
179 | const profile = cli.flags.hasOwnProperty("profile")
180 | ? cli.flags.profile
181 | : guessProfile(term);
182 |
183 | const guess: Guesses = {
184 | term,
185 | profile
186 | };
187 |
188 | if (cli.flags.hasOwnProperty("term") || cli.flags.hasOwnProperty("profile")) {
189 | const unsatisfied = ["term", "profile"].filter(n => !Boolean(guess[n]));
190 |
191 | if (unsatisfied.length > 0 && term !== "hyper") {
192 | throw error(
193 | `svg-term: --term and --profile must be used together, ${unsatisfied.join(
194 | ", "
195 | )} missing`
196 | );
197 | }
198 | }
199 |
200 | const unknown = ensure(["term"], cli.flags, (name, val) => {
201 | if (!cli.flags.hasOwnProperty(name)) {
202 | return null;
203 | }
204 |
205 | if (parsers.TermSchemes.hasOwnProperty(cli.flags.term)) {
206 | return null;
207 | }
208 |
209 | return new TypeError(
210 | `${name} expected to be one of ${Object.keys(parsers.TermSchemes).join(
211 | ", "
212 | )}, received "${val}"`
213 | );
214 | });
215 |
216 | if (unknown.length > 0) {
217 | throw error(`svg-term: ${unknown.map(m => m.message).join("\n")}`);
218 | }
219 |
220 | const [err, theme] = await getTheme(guess);
221 |
222 | if (err) {
223 | throw error(`svg-term: ${err.message}`);
224 | }
225 |
226 | const svg = render(input, {
227 | at: toNumber(cli.flags.at),
228 | cursor: toBoolean(cli.flags.cursor, true),
229 | from: toNumber(cli.flags.from),
230 | paddingX: toNumber(cli.flags.paddingX || cli.flags.padding),
231 | paddingY: toNumber(cli.flags.paddingY || cli.flags.padding),
232 | to: toNumber(cli.flags.to),
233 | height: toNumber(cli.flags.height),
234 | theme,
235 | width: toNumber(cli.flags.width),
236 | window: toBoolean(cli.flags.window, false)
237 | });
238 |
239 | const svgo = new SVGO({
240 | plugins: [{ collapseGroups: false }]
241 | });
242 |
243 | const optimized = toBoolean(cli.flags.optimize, true)
244 | ? await svgo.optimize(svg)
245 | : { data: svg };
246 |
247 | if (typeof cli.flags.out === "string") {
248 | sander.writeFile(cli.flags.out, Buffer.from(optimized.data));
249 | } else {
250 | process.stdout.write(optimized.data);
251 | }
252 | }
253 |
254 | async function command(name: string): Promise {
255 | try {
256 | return (await commandExists(name)) === name;
257 | } catch (err) { // tslint:disable-line no-unused
258 | return false;
259 | }
260 | }
261 |
262 | function ensure(
263 | names: string[],
264 | flags: SvgTermCli["flags"],
265 | predicate: (name: string, val: any) => Error | null
266 | ): Error[] {
267 | return names
268 | .map(name => predicate(name, flags[name]))
269 | .filter(e => e instanceof Error)
270 | .map(e => e as Error);
271 | }
272 |
273 | function cliError(cli: SvgTermCli): (message: string) => SvgTermError {
274 | return message => {
275 | const err: any = new Error(message);
276 | err.help = () => cli.help;
277 |
278 | return err;
279 | };
280 | }
281 |
282 | function getConfig(term: GuessedTerminal): any {
283 | switch (term) {
284 | case GuessedTerminal.terminal: {
285 | return macosAppConfig.sync(term)[0];
286 | }
287 | case GuessedTerminal.iterm2: {
288 | return macosAppConfig.sync(term)[0];
289 | }
290 | default:
291 | return null;
292 | }
293 | }
294 |
295 | function getPresets(term: GuessedTerminal): any {
296 | const config = getConfig(term);
297 |
298 | switch (term) {
299 | case GuessedTerminal.terminal: {
300 | return config["Window Settings"];
301 | }
302 | case GuessedTerminal.iterm2: {
303 | return config["Custom Color Presets"];
304 | }
305 | default:
306 | return null;
307 | }
308 | }
309 |
310 | function guessProfile(term: GuessedTerminal): string | null {
311 | if (os.platform() !== "darwin") {
312 | return null;
313 | }
314 |
315 | const config = getConfig(term);
316 |
317 | if (!config) {
318 | return null;
319 | }
320 |
321 | switch (term) {
322 | case GuessedTerminal.terminal: {
323 | return config["Default Window Settings"];
324 | }
325 | case GuessedTerminal.iterm2: {
326 | return null;
327 | }
328 | default:
329 | return null;
330 | }
331 | }
332 |
333 | async function getInput(cli: SvgTermCli) {
334 | if (cli.flags.command) {
335 | return record(cli.flags.command);
336 | }
337 |
338 | if (cli.flags.in) {
339 | return String(await sander.readFile(cli.flags.in));
340 | }
341 |
342 | if (cli.flags.cast) {
343 | const response = await fetch(
344 | `https://asciinema.org/a/${cli.flags.cast}.cast?dl=true`
345 | );
346 |
347 | return response.text();
348 | }
349 |
350 | return getStdin();
351 | }
352 |
353 | function getParser(term: string) {
354 | switch (term) {
355 | case parsers.TermSchemes.iterm2:
356 | return parsers.iterm2;
357 | case parsers.TermSchemes.konsole:
358 | return parsers.konsole;
359 | case parsers.TermSchemes.remmina:
360 | return parsers.remmina;
361 | case parsers.TermSchemes.terminal:
362 | return parsers.terminal;
363 | case parsers.TermSchemes.terminator:
364 | return parsers.terminator;
365 | case parsers.TermSchemes.termite:
366 | return parsers.termite;
367 | case parsers.TermSchemes.tilda:
368 | return parsers.tilda;
369 | case parsers.TermSchemes.xcfe:
370 | return parsers.xfce;
371 | case parsers.TermSchemes.xresources:
372 | return parsers.xresources;
373 | case parsers.TermSchemes.xterm:
374 | return parsers.xterm;
375 | default:
376 | throw new Error(`unknown term parser: ${term}`);
377 | }
378 | }
379 |
380 | type Result = [Error, null] | [null, T];
381 |
382 | async function getTheme(guess: Guesses): Promise> {
383 | if (guess.term === null && guess.profile === null) {
384 | return [null, null];
385 | }
386 |
387 | const p = guess.profile || "";
388 | const isFileProfile = ["~", "/", "."].indexOf(p.charAt(0)) > -1;
389 |
390 | return isFileProfile
391 | ? parseTheme(guess.term as string, guess.profile as string)
392 | : extractTheme(guess.term as string, guess.profile as string);
393 | }
394 |
395 | async function parseTheme(term: string, input: string): Promise> {
396 | try {
397 | const parser = getParser(term);
398 | const content = String(await sander.readFile(input));
399 |
400 | return [null, parser(content)];
401 | } catch (err) {
402 | return [err, null];
403 | }
404 | }
405 |
406 | async function extractTheme(term: string, name: string): Promise> {
407 | if (!GuessedTerminal.hasOwnProperty(term)) {
408 | return [null, null];
409 | }
410 |
411 | if (os.platform() !== "darwin") {
412 | return [null, null];
413 | }
414 |
415 | if (term === GuessedTerminal.hyper) {
416 | try {
417 | const filename = path.resolve(os.homedir(), ".hyper.js");
418 | const content = String(await sander.readFile(filename));
419 | const result = parsers.hyper(content, {filename});
420 |
421 | return [null, result];
422 | } catch (err) {
423 | return [err, null];
424 | }
425 | }
426 |
427 | const presets = getPresets(term as GuessedTerminal);
428 |
429 | if (!presets) {
430 | return [null, null];
431 | }
432 |
433 | if (!presets.hasOwnProperty(name)) {
434 | const err = new Error(
435 | `profile "${name}" not found for terminal "${term}". Available: ${Object.keys(
436 | presets
437 | ).join(", ")}`
438 | );
439 |
440 | return [err, null];
441 | }
442 |
443 | const theme = presets[name];
444 | const parser = getParser(term);
445 |
446 | if (!theme) {
447 | return [null, null];
448 | }
449 |
450 | switch (term) {
451 | case GuessedTerminal.iterm2:
452 | case GuessedTerminal.terminal:
453 | try {
454 | return [null, parser(plist.build(theme))];
455 | } catch (err) {
456 | return [err, null];
457 | }
458 | default:
459 | return [null, null];
460 | }
461 | }
462 |
463 | async function record(
464 | cmd: string,
465 | options: RecordOptions = {}
466 | ): Promise {
467 | const tmp = tempy.file({ extension: ".json" });
468 |
469 | const result = await execa("asciinema", [
470 | "rec",
471 | "-c",
472 | cmd,
473 | ...(options.title ? ["-t", options.title] : []),
474 | tmp
475 | ]);
476 |
477 | if (result.code > 0) {
478 | throw new Error(
479 | `recording "${cmd}" failed\n${result.stdout}\n${result.stderr}`
480 | );
481 | }
482 |
483 | return String(await sander.readFile(tmp));
484 | }
485 |
486 | function toNumber(input: string | null): number | null {
487 | if (!input) {
488 | return null;
489 | }
490 | const candidate = parseInt(input, 10);
491 | if (isNaN(candidate)) {
492 | return null;
493 | }
494 |
495 | return candidate;
496 | }
497 |
498 | function toBoolean(input: any, fb: boolean): boolean {
499 | if (input === undefined) {
500 | return fb;
501 | }
502 | if (input === "false") {
503 | return false;
504 | }
505 | if (input === "true") {
506 | return true;
507 | }
508 |
509 | return input === true;
510 | }
511 |
512 | function withCli(
513 | fn: (cli: SvgTermCli) => Promise,
514 | help: string = "",
515 | options = {}
516 | ): void {
517 | const unknown: string[] = [];
518 | const cli = meow(help, {
519 | ...options,
520 | unknown: (arg: string) => {
521 | unknown.push(arg);
522 |
523 | return false;
524 | }
525 | });
526 |
527 | if (unknown.length > 0) {
528 | const msg = chalk.red(`svg-term: remove unknown flags ${unknown.join(", ")}`);
529 |
530 | console.error("\n", msg);
531 | console.error(cli.help);
532 | console.error("\n", msg);
533 | process.exit(1);
534 | }
535 |
536 | fn(cli).catch(err => {
537 | const msg = chalk.red(err.message);
538 |
539 | if (typeof err.help === "function") {
540 | console.error("\n", msg);
541 | console.error(err.help());
542 | console.error("\n", msg);
543 | process.exit(1);
544 | }
545 |
546 | setTimeout(() => {
547 | throw err;
548 | }, 0);
549 | });
550 | }
551 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "lib": ["es2015"],
5 | "module": "commonjs",
6 | "noImplicitAny": true,
7 | "outDir": "lib/",
8 | "pretty": true,
9 | "strictNullChecks": true,
10 | "target": "es2015"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint-xo",
4 | "tslint-config-prettier"
5 | ],
6 | "rules": {
7 | "no-var-requires": false,
8 | "no-require-imports": false,
9 | "only-arrow-functions": false
10 | }
11 | }
--------------------------------------------------------------------------------