#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SIMULATE=false
COLS=86
ROWS=24
SVG_TERM=""
SVG_PROFILE=""
function color() {
local color="${1}"
local text="${2}"
echo -e "\033[1;${color}m${text}\033[0m"
}
export PLAY_PS1="$ "
CACHE_DIR=${TMPDIR:-/tmp}/democtl
SELFPATH="$(realpath "$0")"
ARGS=()
export PYTHONPATH="${CACHE_DIR}/py_modules"
export PATH="${PYTHONPATH}/bin:${CACHE_DIR}/node_modules/.bin:${PATH}:${PATH}"
function usage() {
echo "Usage: ${0} <input> <output> [--help] [options...]"
echo " <input> input file"
echo " <output> output file"
echo " --help show this help"
echo " --cols=${COLS} cols of the terminal"
echo " --rows=${ROWS} rows of the terminal"
echo " --ps1=${PLAY_PS1} ps1 of the recording"
echo " --term=${SVG_TERM} terminal type"
echo " --profile=${SVG_PROFILE} terminal profile"
}
function args() {
local arg
while [[ $# -gt 0 ]]; do
arg="$1"
case "${arg}" in
--internal-simulate)
SIMULATE="true"
shift
;;
--cols | --cols=*)
[[ "${arg#*=}" != "${arg}" ]] && COLS="${arg#*=}" || { COLS="${2}" && shift; } || :
shift
;;
--rows | --rows=*)
[[ "${arg#*=}" != "${arg}" ]] && ROWS="${arg#*=}" || { ROWS="${2}" && shift; } || :
shift
;;
--ps1 | --ps1=*)
[[ "${arg#*=}" != "${arg}" ]] && PLAY_PS1="${arg#*=}" || { PLAY_PS1="${2}" && shift; } || :
shift
;;
--term | --term=*)
[[ "${arg#*=}" != "${arg}" ]] && SVG_TERM="${arg#*=}" || { SVG_TERM="${2}" && shift; } || :
shift
;;
--profile | --profile=*)
[[ "${arg#*=}" != "${arg}" ]] && SVG_PROFILE="${arg#*=}" || { SVG_PROFILE="${2}" && shift; } || :
shift
;;
--help)
usage
exit 0
;;
--*)
echo "Unknown argument: ${arg}"
usage
exit 1
;;
*)
ARGS+=("${arg}")
shift
;;
esac
done
}
function command_exist() {
local command="${1}"
type "${command}" >/dev/null 2>&1
}
function install_asciinema() {
if command_exist asciinema; then
return 0
elif command_exist pip3; then
pip3 install asciinema --target "${PYTHONPATH}" >&2
else
echo "asciinema is not installed" >&2
return 1
fi
}
function install_svg_term_cli() {
if command_exist svg-term; then
return 0
elif command_exist npm; then
npm install --save-dev svg-term-cli --prefix "${CACHE_DIR}" >&2
else
echo "svg-term is not installed" >&2
return 1
fi
}
function install_svg_to_video() {
if command_exist svg-to-video; then
return 0
elif command_exist npm; then
npm install --save-dev https://github.com/wzshiming/svg-to-video --prefix "${CACHE_DIR}" >&2
else
echo "svg-to-video is not installed" >&2
return 1
fi
}
function ext_file() {
local file="${1}"
echo "${file##*.}"
}
function ext_replace() {
local file="${1}"
local ext="${2}"
echo "${file%.*}.${ext}"
}
function demo2cast() {
local input="${1}"
local output="${2}"
echo "Recording ${input} to ${output}" >&2
asciinema rec \
"${output}" \
--overwrite \
--cols "${COLS}" \
--rows "${ROWS}" \
--env "" \
--command "bash ${SELFPATH} ${input} --internal-simulate --ps1='${PLAY_PS1}'"
}
function cast2svg() {
local input="${1}"
local output="${2}"
local args=()
echo "Converting ${input} to ${output}" >&2
if [[ "${SVG_TERM}" != "" ]]; then
args+=("--term" "${SVG_TERM}")
fi
if [[ "${SVG_PROFILE}" != "" ]]; then
args+=("--profile" "${SVG_PROFILE}")
fi
svg-term \
--in "${input}" \
--out "${output}" \
--window \
"${args[@]}"
}
function svg2video() {
local input="${1}"
local output="${2}"
echo "Converting ${input} to ${output}" >&2
svg-to-video \
"${input}" \
"${output}" \
--delay-start 5 \
--headless
}
function convert() {
local input="${1}"
local output="${2}"
local castfile
local viedofile
local outext
local inext
outext=$(ext_file "${output}")
inext=$(ext_file "${input}")
case "${outext}" in
cast)
case "${inext}" in
demo)
install_asciinema
demo2cast "${input}" "${output}"
return 0
;;
*)
echo "Unsupported input file type: ${inext}"
return 1
;;
esac
;;
svg)
case "${inext}" in
cast)
install_svg_term_cli
cast2svg "${input}" "${output}"
return 0
;;
demo)
install_asciinema
install_svg_term_cli
castfile=$(ext_replace "${output}" "cast")
demo2cast "${input}" "${castfile}"
cast2svg "${castfile}" "${output}"
return 0
;;
*)
echo "Unsupported input file type: ${inext}"
return 1
;;
esac
;;
mp4)
case "${inext}" in
svg)
install_svg_to_video
svg2video "${input}" "${output}"
return 0
;;
cast)
install_svg_term_cli
install_svg_to_video
viedofile=$(ext_replace "${output}" "svg")
cast2svg "${input}" "${viedofile}"
svg2video "${viedofile}" "${output}"
return 0
;;
demo)
install_asciinema
install_svg_term_cli
install_svg_to_video
viedofile=$(ext_replace "${output}" "svg")
castfile=$(ext_replace "${output}" "cast")
demo2cast "${input}" "${castfile}"
cast2svg "${castfile}" "${viedofile}"
svg2video "${viedofile}" "${output}"
return 0
;;
*)
echo "Unsupported input file type: ${inext}"
return 1
;;
esac
;;
*)
echo "Unsupported output file type: ${outext}"
return 1
;;
esac
}
function br() {
echo
}
function ps1() {
local delay="${1}"
echo -e -n "${PLAY_PS1}"
if [[ "${delay}" != "" ]]; then
sleep "${delay}"
fi
}
function type_message() {
local message="$1"
local delay="${2:-0.02}"
local entry_delay="${3:-0.1}"
for ((i = 0; i < ${#message}; i++)); do
echo -n "${message:$i:1}"
sleep "${delay}"
done
sleep "${entry_delay}"
br
}
function type_and_exec_command() {
local command="$*"
type_message "${command}" 0.01 0.5
eval "${command}"
}
function play_file() {
local file="$1"
while read -r line; do
if [[ "${line}" =~ ^# ]]; then
ps1 0.5
type_message "${line}"
elif [[ "${line}" == "" ]]; then
ps1 2
br
else
ps1 1
type_and_exec_command "${line}"
fi
done <"${file}"
}
function main() {
if [[ "${#ARGS[*]}" -lt 1 ]]; then
usage
exit 1
fi
INPUT_FILE="${ARGS[0]}"
if [[ "${SIMULATE}" == "true" ]]; then
play_file "${INPUT_FILE}"
exit 0
fi
if [[ "${#ARGS[*]}" -gt 1 ]]; then
OUTPUT_FILE="${ARGS[1]}"
else
OUTPUT_FILE="$(ext_replace "${INPUT_FILE}" "svg")"
fi
convert "${INPUT_FILE}" "${OUTPUT_FILE}"
}
args "$@"
main