Logo

Скринкаст терминала

Создание анимированной записи терминала для блога.

ASCIINEMA

Существует целый ряд различных утилит для создания скринкастов терминала. Одна из самых известных – это asciinema, она создает псевдотерминал в котором записывает действия пользователя вместе со всем выводом третьих приложений в файл собственного формата. Для воспроизведения скринкаста предлагается разместить на странице специальный скрипт на js который сформирует видеоплеер и подгрузит указанный файл с данными. Это удобное решение, имеется возможность копировать текст, отлично работает масштабирование, однако минусом является требование поддержки js который может быть отключен по тем или иным причинам (например, как обычно, и бывает у пользователей в даркнете).

Перед публикацией скринкаста от asciinema, необходимо учитывать что в файл возможно попадание значимой информации, например полный путь к текущей директории включающей имя пользователя, дата записи, вид оболочки.

Ниже приведен пример такого скринкаста:

SVG

Другой тип утилит для скринкастов это утилиты, которые создают анимированные svg файлы. Плюсами данного решения так же являются то, что, во-первых, весь текст в графике является текстом, а не изображением и поэтому его легко можно выделять и копировать (впрочем, в macOS уже давно весь текст в изображениях распознается “на лету” и точно так же свободно копируется, очевидно со временем подобное появится и у остальных ОС), а во-вторых, то, что svg представляет векторную графику, а потому отлично масштабируется без потери качества. Собственно получаем аналог прошлого решения, но без требований работы js. Однако и у svg, тем более анимированного, могут быть свои ограничения. В основных современных браузерах он поддерживается, но шаг в сторону и уже могут возникнуть проблемы.

Примером такой утилиты является termtosvg.

При запуске утилиты на M1 из-за lxml 5.1 возникает ошибка “symbol not found in flat namespace (_xmlFree)”. Исправляется с помощью удаления текущей lxml и установки версии 4.9.4: pip3 install lxml==4.9.4

По ссылке приведен пример скринкаста записанного как svg файл. Он приведен именно по ссылке, ибо если разместить на странице одновременно с скринкастом от asciinema (выше), то fps чудовищно падает, а нагрузка на процессор приближается к 100%.

Нагрузка при рендринге svg+asciinema

В целом подобный анимированный svg довольно тяжеловесен и неплохо нагружает процессор. Что так же может являться негативным фактором при выборе подходящего формата.

Дополнительно можно глянуть на утилиты svg-term-cli и asciicast-to-svg. Обе позволяют конвертировать записанный скринкаст asciinema в .svg

GIF, WebP, MP4

Ну и в заключении последняя категория утилит которые пишут скринкасты в виде анимированных растровых изображений (как то GIF или WebP), либо даже в виде полноценного видео (H.264).

Логотип VHS

Вероятно, самое лучшее решение – это VHS. Утилита поддерживает форматы .gif, .webp, .mp4, а так же для задач CI вывод в .txt или .ascii

Ещё одним крайне важным плюсом является не просто использование псевдотерминала где необходимо самостоятельно вводить команды в процессе записи, а создание скриптов для полной автоматизации процесса, когда все команды и действия прописываются заранее, тем самым снижается вероятность ошибок при наборе текста, задержек, а главное появляется свойство точной повторяемости, что крайне важно при подготовке разных версий скринкаста (например, для темной и светлой темы), ну или при необходимости обновить скринкаст под новую версию приложения. Автоматизация выведена на такой высокий уровень (особо актуальный для CI), что vhs можно запускать как сервер, имеется даже встроенная поддержка ssh.

Если ставить vhs из репозитория, например ports, то будет подтянуто огромное количество зависимостей, лучше напрямую скачать бинарный файл. Дополнительно потребуется установить только ffmpeg и ttyd.

Так же особое внимание стоит обратить на то, что при первом запуске vhs установит для себя Chromium в директорию ~/.cache/rod/browser

Пример скринкаста записанного в виде анимированного .gif:

bonsai.gif

Ещё могут быть интересны agg и terminalizer. Обе поддерживают только .gif, но первая занимается конвертацией скринкастов записанных asciinema, а вторая работает вполне самостоятельно (дополнительно предлагая возможность загружать скринкасты к себе на сервер в виде веб-плеера).

Размеры файлов

Таблица позволяющая примерно оценить объемы получаемых скринкастов в различных форматах:

ФайлРазмерAsciinemaРазмер
bonsai.gif110 Кбbonsai.cast23 Кб
bonsai.webp65 Кбasciinema-player.min.js159 Кб
bonsai.mp459 Кбasciinema-player.css54 Кб
bonsai.svg218 КбИтого236 Кб
bonsai.svgz (gzip)25 Кб

Дополнительно

В большинстве случаев скринкасты представлены в виде прямоугольного окна без какого-либо обрамления. Правда, vhs и terminalizer позволяют опционально добавить рамку для мимикрии под некое абстрактное окно приложения, но вот виды и типы рамок заранее определены и не всегда могут быть интересны, в том числе подходить по цветовой палитре.

В качестве решения для сайта можно самостоятельно обрамлять скринкаст какой угодно рамкой. Стандарт CSS поддерживает специальное свойство border-image которое позволяет отрисовывать изображение вокруг указанного элемента. Таким образом можно взять единый шаблон окна, нарезать его на 9 частей и использовать как универсальную рамку. Тем самым значительно сократив и размер изображения скринкаста.

Для примера пусть имеется вот такое крохотное (для сокращения размера и ускорения загрузки страницы) изображение окна терминала:

Шаблон окна

Его размеры 133 х 117 точек при разрешении 144 пикселя на дюйм, вместо обычных 72 (для поддержки дисплеев с высоким разрешением), что дает 266 х 234 пикселя.

Разделим его виртуально на 9 частей, примерно вот так:

Окно разрезано на 9 частей

Очевидно каждая часть становится отдельным элементом и её можно независимо использовать для обрамления скринкаста, именно этим и занимается свойство border-image. Ниже приведен пример селектора класса для тега <img> в котором отображается скринкаст:

.terminal {
	border-top: 39px solid;
	border-left: 45px solid;
	border-right: 45px solid;
	border-bottom: 53px solid;
	
	border-image-slice: 78 90 106 fill;

	border-image-source: "pattern@2x.png";
}

В первых четырех свойствах задается размер рамки, толщина соответственно верхней рамки, левой с правой и нижней. Далее в свойстве border-image-slice указано каким именно образом изображение окна будет нарезаться на те самые 9 частей:

Зоны 1-4 представляют угловые регионы, они используются единожды для формирования конечного изображения рамки.

Зоны 5-8 формируют края, данные участки могут повторяться, чтобы заполнить свои участки рамки. Например, зона 5 имеет ширину в 120 пикселей, соответственно если нужно отобразить рамку шириной в 600 пикселей, данный участок изображения будет отрисован последовательно друг за другом 5 раз.

Зона 9 в данном случае не особо интересна, она не имеет прямого отношения к рамке, но опционально позволяет заполнить всю центральную область. Для этого в свойстве border-image-slice необходимо задать ключевое слово fill.

Теперь рассмотрим как определяется размер зон. Его можно задать четырьмя различными способами:

/* единое значение для всех 4 регионов */
border-image-slice: 100;

/* верх и низ | лево и право */
border-image-slice: 110 90;

/* верх | лево и право | низ */
border-image-slice: 78 90 106;

/* верх | право | низ | лево */
border-image-slice: 78 90 106 80;

В рассматриваем случае используется единое значение для отступов слева и справа, но разное для верха и низа. Низ значительно больше из-за наличия там тени для окна.

Особо стоит обратить внимание на разные значения для border-image-slice и border-top|left|right|bottom. В обоих случаех подразумевается что значения заданы в пикселях. Но есть нюанс. Для border-image-slice заданы вполне конкретные пиксели изображения, тогда как у остальных 4 свойств хоть и задана единица измерения пиксель (px), означать она может разное. Для обычных дисплеев это стандартный пиксель – точка на экране, для устройств с высоким разрешением 1 такой пиксель может означать несколько реальных пикселей (считается по формуле 1px = 1in / 96). Поэтому учитывая, что в качестве шаблона используется изображение для дисплеев с высоким разрешением, значения для первого свойства в 2 раза выше остальных.

Ну и теперь собственно наглядный пример как такая рамка обрамляет скринкаст:

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="bonsai-dark@2x.gif">
  <img class="terminal" src="bonsai@2x.gif" width="380">
</picture>
bonsai.gif

На этом всё.