Animated terminal in presentations

Recently I attended several conferences and I needed to show the audience how to do some things using terminal. Today I want to share my solution to create somethings like this:

Recording terminal

We don’t want to record real videos and upload them to YouTube/Vimeo. Terminal is simpler than that — you don’t have fancy graphics there with millions of pixels with unique data. Real videos are heavy for encoding, decoding and also use a lot of disk space.

Instead we want to capture the real data. It turns out that there is already a project that captures all the streams in terminal and it’s called asciinema.

Unfortunately it turns out that I’m bad at recording: I make mistakes when I type, my typing speed is sometimes too fast or slow and I do huge pauses. So I wanted something better, something where I create a script for the recording and then render it:

Hello world example
title: a beautiful movie
options:
width: 80
height: 24
wait: true
typing_delay_s: 0.1
reading_delay_s: 1
pre-run:
- pause: 1
- command: clear
post-run:
- pause: 1
scenes:
- command: "echo 'hello world!'"

So it turns out that there is already a project to do this, but it doesn’t provide a lot of customisation options, so I decided to rewrite it and add more features. Meet spielbash in Ruby.

The way it works is:

  • Deserialise actions from scenario’s yaml file
  • Start a tmux session
  • Start asciinema recording
  • Execute actions
  • Profit

Let’s talk about some caveats I found interesting during the implementation.

Window size

The size of tmux session window is calculated as minimal dimensions of all connected clients, so in order to resize the recording we need to connect with client that has desired window heigh & width.

We can use resize for this task, for example to get 80x24 window:

$ resize -s 24 80

We need to execute this before we connect to our tmux session.

Executing commands

tmux supports sending keystrokes, for example to emulate typing Hello:

$ tmux send-keys -t session_name Hello C-m

This will also emulate carriage-return at the end with C-m. This command uses an already started tmux session, you can find the name of open sessions by using tmux ls command.

Waiting

After you execute a command you probably want to wait for the end of execution. But how do you do this when you’re just sending key-strokes?

The answer is found in the way how process is created by OS. Each process actually has a parent, when you execute something in your terminal most of the time your app will have the shell process as a parent. In our case we execute everything in a tmux session process. So in order to understand if we have some app running we need to check if our tmux session has any child processes.

$ TMUX_PID=$(tmux list-panes -F '#{pane_pid}' -t session_name)
$ pgrep -P $TMUX_PID

This will give us a list of all process ids that are running inside our tmux session.

Recording

Before starting the recording you probably want a clean state of terminal, that’s why I implemented a special group of commands pre-run and post-run. They’re executed before and after the recording.

After executing the pre-run we need to start the actual recording. Asciinema actually has an options to run a custom command instead of your default shell and we’ll use it to our advantage:

$ asciinema rec -c "tmux attach -t session_name"

This will connect to existing tmux session instead of default shell.

After this we execute all actions defined in the script, wait when appropriate and then stop the recording using exit.

Virtualising the environment

After I finished the described implementation I still had one problem: I couldn’t record other environments like ubuntu, centos on my MacOS. I tried to start something in docker but I couldn’t figure out the waiting part for a day or two. The problem is we start docker container and then check what are the child processes and we get nothing:

$ docker run -it ubuntu
$ sleep 42

The problem is that sleep 42 doesn’t belong to the tree of processes started from running docker process. Why?

It turns out that when you actually start docker container on your Mac the real process is running inside the VM with linux, so the sleep 42 process doesn’t really fork from the docker process running in your shell.

Before solving this we need to understand what is the actual problem, not just the technical one: we changed the environment and our check for running processes also needs to be changed for whole duration of using temporary environment.

Instead of pgrep on our machine we now should consider running something inside the same docker container, such as

$ docker exec container_name pgrep -P 1

We still use the same pgrep, but now we check for child processes started from parent PID 1 since that is our process for shell inside the container. If you’re wondering why it’s PID 1 then the answer is that it‘s actually the first process running inside this container: there was nothing before like init process in real OS that actually has boot sequence.

So in order to change the environment we will add the following to the scenario:

pre-run:
- new_env: 'docker run -it --rm --name ubuntu ubuntu bash'
wait_check_cmd: 'docker exec ubuntu pgrep -P 1'

And to exit environment and go back to previous logic of checking for waiting processes we basically just exit the container by executing exit command:

post-run:
- delete_env: 'exit'

All the actions we execute between new_env and delete_env will be using wait_check_cmd to check if we need to wait for command to finish. For example consider the following scenario:

pre-run:
- new_env: 'docker run -it --rm --name ubuntu ubuntu bash'
wait_check_cmd: 'docker exec ubuntu pgrep -P 1'
post-run:
- delete_env: 'exit'
scenes:
- command: sleep 5

sleep 5 will be executed already inside ubuntu docker image and to check that we actually have something running it will automatically use docker exec ubuntu pgrep -P 1. If sleep is still running it will output PID of it’s process, otherwise the output will be empty and we will proceed to the next action. After we finish all the recording we will also exit from the ubuntu container via exit.

Result

In the end we can render complex scenarios that allow us to create temporary environments using docker, record terminal and then tear everything down without any trace of temporary files created.

Recording using virtualization

Embedding

In order to embed the created asciinema recoding you sometimes need to convert it into gif, for that you can use existing projects like asciicast2gif:

$ docker run --rm -it -v /tmp:/data asciinema/asciicast2gif /data/recording.json recording.gif

Links

https://github.com/Malinskiy/spielbash

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Anton Malinskiy

Anton Malinskiy

Software engineer & IT conference speaker; Landscape photographer + occasional portraits; Music teacher: piano guitar violin; Bike traveller, gymkhana