|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Copyright 2024 The Kubernetes Authors. |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +# you may not use this file except in compliance with the License. |
| 6 | +# You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | + |
| 16 | +set -o errexit |
| 17 | +set -o nounset |
| 18 | +set -o pipefail |
| 19 | + |
| 20 | +SIMULATE=false |
| 21 | +COLS=86 |
| 22 | +ROWS=24 |
| 23 | +SVG_TERM="vt100" |
| 24 | +SVG_PROFILE="" |
| 25 | + |
| 26 | +function color() { |
| 27 | + local color="${1}" |
| 28 | + local text="${2}" |
| 29 | + echo -e "\033[1;${color}m${text}\033[0m" |
| 30 | +} |
| 31 | + |
| 32 | +export PLAY_PS1="$ " |
| 33 | + |
| 34 | +CACHE_DIR=${TMPDIR:-/tmp}/democtl |
| 35 | +SELFPATH="$(realpath "$0")" |
| 36 | +ARGS=() |
| 37 | + |
| 38 | +export PYTHONPATH="${CACHE_DIR}/py_modules" |
| 39 | +export PATH="${PYTHONPATH}/bin:${CACHE_DIR}/node_modules/.bin:${PATH}:${PATH}" |
| 40 | + |
| 41 | +function usage() { |
| 42 | + echo "Usage: ${0} <input> <output> [--help] [options...]" |
| 43 | + echo " <input> input file" |
| 44 | + echo " <output> output file" |
| 45 | + echo " --help show this help" |
| 46 | + echo " --cols=${COLS} cols of the terminal" |
| 47 | + echo " --rows=${ROWS} rows of the terminal" |
| 48 | + echo " --ps1=${PLAY_PS1} ps1 of the recording" |
| 49 | + echo " --term=${SVG_TERM} terminal type" |
| 50 | + echo " --profile=${SVG_PROFILE} terminal profile" |
| 51 | +} |
| 52 | + |
| 53 | +# args parses the arguments. |
| 54 | +function args() { |
| 55 | + local arg |
| 56 | + |
| 57 | + while [[ $# -gt 0 ]]; do |
| 58 | + arg="$1" |
| 59 | + case "${arg}" in |
| 60 | + --internal-simulate) |
| 61 | + SIMULATE="true" |
| 62 | + shift |
| 63 | + ;; |
| 64 | + --cols | --cols=*) |
| 65 | + [[ "${arg#*=}" != "${arg}" ]] && COLS="${arg#*=}" || { COLS="${2}" && shift; } || : |
| 66 | + shift |
| 67 | + ;; |
| 68 | + --rows | --rows=*) |
| 69 | + [[ "${arg#*=}" != "${arg}" ]] && ROWS="${arg#*=}" || { ROWS="${2}" && shift; } || : |
| 70 | + shift |
| 71 | + ;; |
| 72 | + --ps1 | --ps1=*) |
| 73 | + [[ "${arg#*=}" != "${arg}" ]] && PLAY_PS1="${arg#*=}" || { PLAY_PS1="${2}" && shift; } || : |
| 74 | + shift |
| 75 | + ;; |
| 76 | + --term | --term=*) |
| 77 | + [[ "${arg#*=}" != "${arg}" ]] && SVG_TERM="${arg#*=}" || { SVG_TERM="${2}" && shift; } || : |
| 78 | + shift |
| 79 | + ;; |
| 80 | + --profile | --profile=*) |
| 81 | + [[ "${arg#*=}" != "${arg}" ]] && SVG_PROFILE="${arg#*=}" || { SVG_PROFILE="${2}" && shift; } || : |
| 82 | + shift |
| 83 | + ;; |
| 84 | + --help) |
| 85 | + usage |
| 86 | + exit 0 |
| 87 | + ;; |
| 88 | + --*) |
| 89 | + echo "Unknown argument: ${arg}" |
| 90 | + usage |
| 91 | + exit 1 |
| 92 | + ;; |
| 93 | + *) |
| 94 | + ARGS+=("${arg}") |
| 95 | + shift |
| 96 | + ;; |
| 97 | + esac |
| 98 | + done |
| 99 | +} |
| 100 | + |
| 101 | +# command_exist checks if the command exists. |
| 102 | +function command_exist() { |
| 103 | + local command="${1}" |
| 104 | + type "${command}" >/dev/null 2>&1 |
| 105 | +} |
| 106 | + |
| 107 | +# install_asciinema installs asciinema. |
| 108 | +function install_asciinema() { |
| 109 | + if command_exist asciinema; then |
| 110 | + return 0 |
| 111 | + elif command_exist pip3; then |
| 112 | + pip3 install asciinema --target "${PYTHONPATH}" >&2 |
| 113 | + else |
| 114 | + echo "asciinema is not installed" >&2 |
| 115 | + return 1 |
| 116 | + fi |
| 117 | +} |
| 118 | + |
| 119 | +# install_svg_term_cli installs svg-term-cli. |
| 120 | +function install_svg_term_cli() { |
| 121 | + if command_exist svg-term; then |
| 122 | + return 0 |
| 123 | + elif command_exist npm; then |
| 124 | + npm install --save-dev svg-term-cli --prefix "${CACHE_DIR}" >&2 |
| 125 | + else |
| 126 | + echo "svg-term is not installed" >&2 |
| 127 | + return 1 |
| 128 | + fi |
| 129 | +} |
| 130 | + |
| 131 | +# install_svg_to_video installs svg-to-video. |
| 132 | +function install_svg_to_video() { |
| 133 | + if command_exist svg-to-video; then |
| 134 | + return 0 |
| 135 | + elif command_exist npm; then |
| 136 | + npm install --save-dev https://github.com/wzshiming/svg-to-video --prefix "${CACHE_DIR}" >&2 |
| 137 | + else |
| 138 | + echo "svg-to-video is not installed" >&2 |
| 139 | + return 1 |
| 140 | + fi |
| 141 | +} |
| 142 | + |
| 143 | +# ext_file returns the extension of the input file. |
| 144 | +function ext_file() { |
| 145 | + local file="${1}" |
| 146 | + echo "${file##*.}" |
| 147 | +} |
| 148 | + |
| 149 | +# ext_replace replaces the extension of the input file with the output extension. |
| 150 | +function ext_replace() { |
| 151 | + local file="${1}" |
| 152 | + local ext="${2}" |
| 153 | + echo "${file%.*}.${ext}" |
| 154 | +} |
| 155 | + |
| 156 | +# demo2cast converts the input demo file to the output cast file. |
| 157 | +function demo2cast() { |
| 158 | + local input="${1}" |
| 159 | + local output="${2}" |
| 160 | + echo "Recording ${input} to ${output}" >&2 |
| 161 | + |
| 162 | + asciinema rec \ |
| 163 | + "${output}" \ |
| 164 | + --overwrite \ |
| 165 | + --cols "${COLS}" \ |
| 166 | + --rows "${ROWS}" \ |
| 167 | + --env "" \ |
| 168 | + --command "bash ${SELFPATH} ${input} --internal-simulate --ps1='${PLAY_PS1}'" |
| 169 | +} |
| 170 | + |
| 171 | +# cast2svg converts the input cast file to the output svg file. |
| 172 | +function cast2svg() { |
| 173 | + local input="${1}" |
| 174 | + local output="${2}" |
| 175 | + local args=() |
| 176 | + echo "Converting ${input} to ${output}" >&2 |
| 177 | + |
| 178 | + if [[ "${SVG_TERM}" != "" ]]; then |
| 179 | + args+=("--term" "${SVG_TERM}") |
| 180 | + fi |
| 181 | + |
| 182 | + if [[ "${SVG_PROFILE}" != "" ]]; then |
| 183 | + args+=("--profile" "${SVG_PROFILE}") |
| 184 | + fi |
| 185 | + svg-term \ |
| 186 | + --in "${input}" \ |
| 187 | + --out "${output}" \ |
| 188 | + --window \ |
| 189 | + "${args[@]}" |
| 190 | +} |
| 191 | + |
| 192 | +# svg2video converts the input svg file to the output video file. |
| 193 | +function svg2video() { |
| 194 | + local input="${1}" |
| 195 | + local output="${2}" |
| 196 | + echo "Converting ${input} to ${output}" >&2 |
| 197 | + |
| 198 | + svg-to-video \ |
| 199 | + "${input}" \ |
| 200 | + "${output}" \ |
| 201 | + --delay-start 5 \ |
| 202 | + --headless |
| 203 | +} |
| 204 | + |
| 205 | +# convert converts the input file to the output file. |
| 206 | +# The input file can be a demo, cast, or svg file. |
| 207 | +# The output file can be a cast, svg, or mp4 file. |
| 208 | +function convert() { |
| 209 | + local input="${1}" |
| 210 | + local output="${2}" |
| 211 | + |
| 212 | + local castfile |
| 213 | + local viedofile |
| 214 | + |
| 215 | + local outext |
| 216 | + local inext |
| 217 | + |
| 218 | + outext=$(ext_file "${output}") |
| 219 | + inext=$(ext_file "${input}") |
| 220 | + case "${outext}" in |
| 221 | + cast) |
| 222 | + case "${inext}" in |
| 223 | + demo) |
| 224 | + install_asciinema |
| 225 | + |
| 226 | + demo2cast "${input}" "${output}" |
| 227 | + return 0 |
| 228 | + ;; |
| 229 | + *) |
| 230 | + echo "Unsupported input file type: ${inext}" |
| 231 | + return 1 |
| 232 | + ;; |
| 233 | + esac |
| 234 | + ;; |
| 235 | + svg) |
| 236 | + case "${inext}" in |
| 237 | + cast) |
| 238 | + install_svg_term_cli |
| 239 | + |
| 240 | + cast2svg "${input}" "${output}" |
| 241 | + return 0 |
| 242 | + ;; |
| 243 | + demo) |
| 244 | + install_asciinema |
| 245 | + install_svg_term_cli |
| 246 | + |
| 247 | + castfile=$(ext_replace "${output}" "cast") |
| 248 | + demo2cast "${input}" "${castfile}" |
| 249 | + cast2svg "${castfile}" "${output}" |
| 250 | + return 0 |
| 251 | + ;; |
| 252 | + *) |
| 253 | + echo "Unsupported input file type: ${inext}" |
| 254 | + return 1 |
| 255 | + ;; |
| 256 | + esac |
| 257 | + ;; |
| 258 | + mp4) |
| 259 | + case "${inext}" in |
| 260 | + svg) |
| 261 | + install_svg_to_video |
| 262 | + |
| 263 | + svg2video "${input}" "${output}" |
| 264 | + return 0 |
| 265 | + ;; |
| 266 | + cast) |
| 267 | + install_svg_term_cli |
| 268 | + install_svg_to_video |
| 269 | + |
| 270 | + viedofile=$(ext_replace "${output}" "svg") |
| 271 | + cast2svg "${input}" "${viedofile}" |
| 272 | + svg2video "${viedofile}" "${output}" |
| 273 | + return 0 |
| 274 | + ;; |
| 275 | + demo) |
| 276 | + install_asciinema |
| 277 | + install_svg_term_cli |
| 278 | + install_svg_to_video |
| 279 | + |
| 280 | + viedofile=$(ext_replace "${output}" "svg") |
| 281 | + castfile=$(ext_replace "${output}" "cast") |
| 282 | + demo2cast "${input}" "${castfile}" |
| 283 | + cast2svg "${castfile}" "${viedofile}" |
| 284 | + svg2video "${viedofile}" "${output}" |
| 285 | + return 0 |
| 286 | + ;; |
| 287 | + *) |
| 288 | + echo "Unsupported input file type: ${inext}" |
| 289 | + return 1 |
| 290 | + ;; |
| 291 | + esac |
| 292 | + ;; |
| 293 | + *) |
| 294 | + echo "Unsupported output file type: ${outext}" |
| 295 | + return 1 |
| 296 | + ;; |
| 297 | + esac |
| 298 | +} |
| 299 | + |
| 300 | +# br prints a new line. |
| 301 | +# this function is used to simulate typing. |
| 302 | +function br() { |
| 303 | + echo |
| 304 | +} |
| 305 | + |
| 306 | +# ps1 prints the ps1 with a delay. |
| 307 | +# this function is used to simulate typing. |
| 308 | +function ps1() { |
| 309 | + local delay="${1}" |
| 310 | + echo -e -n "${PLAY_PS1}" |
| 311 | + if [[ "${delay}" != "" ]]; then |
| 312 | + sleep "${delay}" |
| 313 | + fi |
| 314 | +} |
| 315 | + |
| 316 | +# type_message prints a message to stdout at a human hand speed. |
| 317 | +# this function is used to simulate typing. |
| 318 | +function type_message() { |
| 319 | + local message="$1" |
| 320 | + local delay="${2:-0.02}" |
| 321 | + local entry_delay="${3:-0.1}" |
| 322 | + for ((i = 0; i < ${#message}; i++)); do |
| 323 | + echo -n "${message:$i:1}" |
| 324 | + sleep "${delay}" |
| 325 | + done |
| 326 | + sleep "${entry_delay}" |
| 327 | + br |
| 328 | +} |
| 329 | + |
| 330 | +# type_and_exec_command prints a command to stdout and executes it. |
| 331 | +# this function is used to simulate typing. |
| 332 | +function type_and_exec_command() { |
| 333 | + local command="$*" |
| 334 | + type_message "${command}" 0.01 0.5 |
| 335 | + eval "${command}" |
| 336 | +} |
| 337 | + |
| 338 | +# play_file plays a file line by line. |
| 339 | +# this function is used to simulate typing. |
| 340 | +function play_file() { |
| 341 | + local file="$1" |
| 342 | + while read -r line; do |
| 343 | + if [[ "${line}" =~ ^# ]]; then |
| 344 | + ps1 0.5 |
| 345 | + type_message "${line}" |
| 346 | + elif [[ "${line}" == "" ]]; then |
| 347 | + ps1 2 |
| 348 | + br |
| 349 | + else |
| 350 | + ps1 1 |
| 351 | + type_and_exec_command "${line}" |
| 352 | + fi |
| 353 | + done <"${file}" |
| 354 | +} |
| 355 | + |
| 356 | +function main() { |
| 357 | + if [[ "${#ARGS[*]}" -lt 1 ]]; then |
| 358 | + usage |
| 359 | + exit 1 |
| 360 | + fi |
| 361 | + |
| 362 | + INPUT_FILE="${ARGS[0]}" |
| 363 | + |
| 364 | + # Only for asciinema recording command. |
| 365 | + if [[ "${SIMULATE}" == "true" ]]; then |
| 366 | + play_file "${INPUT_FILE}" |
| 367 | + exit 0 |
| 368 | + fi |
| 369 | + |
| 370 | + if [[ "${#ARGS[*]}" -gt 1 ]]; then |
| 371 | + OUTPUT_FILE="${ARGS[1]}" |
| 372 | + else |
| 373 | + # If the output file is not specified, use the same name as the input file. |
| 374 | + OUTPUT_FILE="$(ext_replace "${INPUT_FILE}" "svg")" |
| 375 | + fi |
| 376 | + |
| 377 | + convert "${INPUT_FILE}" "${OUTPUT_FILE}" |
| 378 | +} |
| 379 | + |
| 380 | +args "$@" |
| 381 | +main |
0 commit comments