├── README.md ├── config.yaml ├── go.mod ├── main.go ├── samples ├── sample-demo-magic-small-compressed.gif └── sample-golang-demo-small-compressed.gif └── template.sh /README.md: -------------------------------------------------------------------------------- 1 | # Template for CLI-based Demos/Showcases 2 | 3 | Templates and config for running scripted and/or pre-recorded CLI demos. 4 | 5 | You can find more information about this repository and how to use it in following blog post: 6 | 7 | - [Make Your CLI Demos a Breeze with Zero Stress and Zero Mistakes](https://martinheinz.dev/blog/94) 8 | 9 | --- 10 | 11 | 12 | 13 | ----- 14 | 15 | If you find this useful, you can support me on Ko-Fi (Donations are always appreciated, but never required): 16 | 17 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/K3K6F4XN6) 18 | 19 | ## Scripted Demo 20 | 21 | Repository provides template for 2 scripting tools, below is setup needed for each of them. 22 | 23 | Setup (`demo-magic`): 24 | 25 | ```shell 26 | # Install "Pipe Viewer" for simulated typing 27 | # MacOS: 28 | brew install pv 29 | # Ubuntu: 30 | apt-get install pv 31 | 32 | git clone https://github.com/paxtonhare/demo-magic.git 33 | cp demo-magic/demo-magic.sh demo-magic.sh 34 | ``` 35 | 36 | To create and run a new demo: 37 | 38 | ```shell 39 | cp template.sh my-cool-demo.sh 40 | # Edit 'my-cool-demo.sh' 41 | 42 | # Run 43 | ./my-cool-demo.sh 44 | ``` 45 | 46 | --- 47 | 48 | Setup (`https://github.com/saschagrunert/demo`): 49 | 50 | ```shell 51 | go get github.com/saschagrunert/demo@latest 52 | 53 | # Edit 'main.go' 54 | 55 | go build 56 | ./cli-demo --all # or change 'all' to name of specific demo run 57 | ``` 58 | 59 | ## Recording Demo 60 | 61 | ```shell 62 | # You might need 'sudo' 63 | npm install -g terminalizer 64 | 65 | # https://github.com/faressoft/terminalizer/issues/29 66 | # Very careful with below command! 67 | sudo chown -R /usr/lib/node_modules/terminalizer/render/ 68 | 69 | terminalizer init 70 | # The global config directory is created at 71 | # /home/martin/.terminalizer 72 | 73 | # Adjust config.yaml... (e.g., change 'title' field) 74 | 75 | # Create copy of default config in current directory 76 | terminalizer config 77 | 78 | # Start recording: 79 | terminalizer record demo --config config.yaml 80 | 81 | # ... run some commands 82 | 83 | # CTRL+D to stop recording... 84 | 85 | # Successfully Recorded 86 | # The recording data is saved into the file: 87 | # /home/.../demo.yml 88 | 89 | terminalizer play demo # To check 90 | terminalizer render demo # To render 91 | ``` 92 | 93 | ## Workflow 94 | 95 | Example usage/streamlined workflow: 96 | 97 | ```shell 98 | # Create demo: 99 | 100 | # ... Edit 'main.go' 101 | go build 102 | # OR 103 | cp template.sh my-cool-demo.sh 104 | # Edit 'my-cool-demo.sh' 105 | 106 | # ----- 107 | 108 | # Start recording: 109 | terminalizer record demo 110 | 111 | # Start scripted demo: 112 | ./cli-demo --all -i 113 | # OR 114 | ./my-cool-demo.sh 115 | 116 | # ... Go through the demo 117 | # CTRL+D 118 | 119 | terminalizer play demo # To check 120 | # ... Adjust demo.yml 121 | terminalizer render demo # To render 122 | ``` 123 | 124 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Specify a command to be executed 2 | # like `/bin/bash -l`, `ls`, or any other commands 3 | # the default is bash for Linux 4 | # or powershell.exe for Windows 5 | command: null 6 | 7 | # Specify the current working directory path 8 | # the default is the current working directory path 9 | cwd: null 10 | 11 | # Export additional ENV variables 12 | env: 13 | recording: true 14 | 15 | # Explicitly set the number of columns 16 | # or use `auto` to take the current 17 | # number of columns of your shell 18 | cols: auto 19 | 20 | # Explicitly set the number of rows 21 | # or use `auto` to take the current 22 | # number of rows of your shell 23 | rows: auto 24 | 25 | # Amount of times to repeat GIF 26 | # If value is -1, play once 27 | # If value is 0, loop indefinitely 28 | # If value is a positive number, loop n times 29 | repeat: 0 30 | 31 | # Quality 32 | # 1 - 100 33 | quality: 100 34 | 35 | # Delay between frames in ms 36 | # If the value is `auto` use the actual recording delays 37 | frameDelay: 150 38 | 39 | # Maximum delay between frames in ms 40 | # Ignored if the `frameDelay` isn't set to `auto` 41 | # Set to `auto` to prevent limiting the max idle time 42 | maxIdleTime: 2000 43 | 44 | # The surrounding frame box 45 | # The `type` can be null, window, floating, or solid` 46 | # To hide the title use the value null 47 | # Don't forget to add a backgroundColor style with a null as type 48 | frameBox: 49 | type: floating 50 | title: CLI Showcase 51 | style: 52 | backgroundColor: "#263238" 53 | border: 0px black solid 54 | # boxShadow: none 55 | # margin: 0px 56 | 57 | # Add a watermark image to the rendered gif 58 | # You need to specify an absolute path for 59 | # the image on your machine or a URL, and you can also 60 | # add your own CSS styles 61 | watermark: 62 | imagePath: null 63 | style: 64 | position: absolute 65 | right: 15px 66 | bottom: 15px 67 | width: 100px 68 | opacity: 0.9 69 | 70 | # Cursor style can be one of 71 | # `block`, `underline`, or `bar` 72 | cursorStyle: block 73 | 74 | # Font family 75 | # You can use any font that is installed on your machine 76 | # in CSS-like syntax 77 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 78 | 79 | # The size of the font 80 | fontSize: 12 81 | 82 | # The height of lines 83 | lineHeight: 1 84 | 85 | # The spacing between letters 86 | letterSpacing: 0 87 | 88 | # Theme 89 | theme: 90 | background: "transparent" 91 | foreground: "#afafaf" 92 | cursor: "#c7c7c7" 93 | black: "#2E3436" # "#232628" 94 | red: "#CC0000" # "#fc4384" 95 | green: "#4E9A06" # "#b3e33b" 96 | yellow: "C4A000" # "#ffa727" 97 | blue: "#3465A4" # "#75dff2" 98 | magenta: "#75507B" # "#ae89fe" 99 | cyan: "#06989A" # "#708387" 100 | white: "#D3D7CF" # "#d5d5d0" 101 | brightBlack: "#555753" # "#626566" 102 | brightRed: "#EF2929" # "#ff7fac" 103 | brightGreen: "#8AE234" # "#c8ed71" 104 | brightYellow: "#FCE94F" # "#ebdf86" 105 | brightBlue: "#729FCF" # "#75dff2" 106 | brightMagenta: "#AD7FA8" # "#ae89fe" 107 | brightCyan: "#34E2E2" # "#b1c6ca" 108 | brightWhite: "#EEEEEC" # "#f9f9f4" 109 | 110 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cli-demo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 7 | github.com/gookit/color v1.5.2 // indirect 8 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 9 | github.com/saschagrunert/demo v0.0.0-20230228152132-acc488841b08 // indirect 10 | github.com/urfave/cli/v2 v2.24.4 // indirect 11 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 12 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 13 | golang.org/x/sys v0.5.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | demo "github.com/saschagrunert/demo" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func main() { 9 | d := demo.New() 10 | 11 | d.Name = "A demo of demo" 12 | d.Usage = "Examples of how to use this" 13 | d.HideVersion = true 14 | 15 | d.Setup(setup) 16 | d.Cleanup(cleanup) 17 | 18 | d.Add(simple(), "simple", "Simple commands demo") 19 | d.Add(longRunning(), "long-run", "Long-running commands demo") 20 | d.Add(errorProne(), "error-prone", "Error-prone commands demo") 21 | d.Add(longTyping(), "long-typing", "Commands that require a lot of typing") 22 | 23 | d.Run() 24 | } 25 | 26 | func simple() *demo.Run { 27 | r := demo.NewRun( 28 | "Simple commands demo", 29 | ) 30 | 31 | r.Step(demo.S( 32 | "Simple commands:", 33 | ), nil) 34 | 35 | r.Step(nil, demo.S( 36 | "ps aux | head", 37 | )) 38 | 39 | r.Step(nil, demo.S( 40 | "ls -l", 41 | )) 42 | 43 | r.Step(nil, demo.S( 44 | "cat some-file", 45 | )) 46 | 47 | return r 48 | } 49 | 50 | func longRunning() *demo.Run { 51 | r := demo.NewRun( 52 | "Long-running commands demo", 53 | ) 54 | 55 | r.Step(demo.S( 56 | "Long running:", 57 | ), demo.S( 58 | "docker build -t some-app .", 59 | )) 60 | 61 | return r 62 | } 63 | 64 | func errorProne() *demo.Run { 65 | r := demo.NewRun( 66 | "Error-prone commands demo", 67 | ) 68 | 69 | r.Step(demo.S( 70 | "Error prone (dependant on external factors like network):", 71 | ), nil) 72 | 73 | r.Step(nil, demo.S( 74 | "curl --silent -X GET https://httpstat.us/418 -H \"Accept: application/json\" | jq .", 75 | )) 76 | 77 | r.Step(nil, demo.S( 78 | "curl -i -X POST \"https://httpbin.org/status/204\" --data '{\"some\":\"data\"}'", 79 | )) 80 | 81 | r.Step(nil, demo.S( 82 | "wget -q --show-progress -O some-file.txt https://raw.githubusercontent.com/octocat/Hello-World/master/README", 83 | )) 84 | 85 | return r 86 | } 87 | 88 | func longTyping() *demo.Run { 89 | r := demo.NewRun( 90 | "Commands that require a lot of typing", 91 | ) 92 | 93 | r.Step(demo.S( 94 | "Hard to type:", 95 | ), demo.S( 96 | "openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes ", 97 | "-keyout example.key -out example.crt -subj \"/CN=example.com\"", 98 | "-addext \"subjectAltName=DNS:example.com,DNS:www.example.net,IP:10.0.0.1\" 2>/dev/null", 99 | )) 100 | 101 | r.Step(nil, demo.S( 102 | "ls -l | grep example", 103 | )) 104 | 105 | return r 106 | } 107 | 108 | func setup(ctx *cli.Context) error { 109 | // Ensure can be used for easy sequential command execution 110 | return demo.Ensure( 111 | "echo 'Here\nis\nsome\ndata\nfor\nyou' > some-file", 112 | ) 113 | } 114 | 115 | func cleanup(ctx *cli.Context) error { 116 | return demo.Ensure( 117 | "rm some-file some-file.txt || true", 118 | "docker rmi some-app || true", 119 | "rm example.crt example.key || true", 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /samples/sample-demo-magic-small-compressed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinHeinz/cli-showcase-setup/4b9d615077e289f66c96636d63d4ea18dbafdf57/samples/sample-demo-magic-small-compressed.gif -------------------------------------------------------------------------------- /samples/sample-golang-demo-small-compressed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinHeinz/cli-showcase-setup/4b9d615077e289f66c96636d63d4ea18dbafdf57/samples/sample-golang-demo-small-compressed.gif -------------------------------------------------------------------------------- /template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://github.com/paxtonhare/demo-magic/blob/master/samples/demo-template.sh 4 | 5 | source demo-magic.sh 6 | 7 | DEMO_PROMPT="${GREEN}➜ ${CYAN}\W ${COLOR_RESET}" 8 | # speed at which to simulate typing. bigger num = faster 9 | TYPE_SPEED=30 10 | 11 | function comment() { 12 | cmd=$DEMO_COMMENT_COLOR$1$COLOR_RESET 13 | echo -en "$cmd"; echo "" 14 | } 15 | 16 | clear 17 | 18 | comment "# Simple commands:" 19 | pe 'ps aux | head' 20 | pe 'ls -l' 21 | 22 | comment "# Print and execute immediately" 23 | pei 'cat some-file' 24 | echo 25 | 26 | comment "# Long running:" 27 | pe 'docker build -t some-app .' 28 | 29 | comment "# Error prone (dependant on external factors like network):" 30 | 31 | pe 'curl --silent -X GET https://httpstat.us/418 -H "Accept: application/json" | jq .' 32 | 33 | pe 'curl -i -X POST "https://httpbin.org/status/204" --data "{'some':'data'}"' 34 | 35 | pe 'wget -q --show-progress -O some-file.txt https://raw.githubusercontent.com/octocat/Hello-World/master/README' 36 | 37 | comment "# Hard to type:" 38 | 39 | pe 'openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout example.key -out example.crt -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:www.example.net,IP:10.0.0.1" 2>/dev/null' 40 | 41 | comment "# Enter interactive mode..." 42 | cmd # Run 'ls -l | grep example' to show result of 'openssl ...' 43 | 44 | pe "clear" 45 | --------------------------------------------------------------------------------