Skip to content

Instantly share code, notes, and snippets.

@ayosec
Last active Jul 14, 2022
Embed
What would you like to do?
Show image thumbnails using Sixel graphics.
#!/bin/bash
#
# Usage:
#
# $ list-images [files]*
#
# This script is a demo to show how to use hyperlinks with images in Alacritty.
# It can be used on any terminal emulator with Sixel support, but it is not
# intended to be used for regular use, mostly because it lacks a proper way to
# handle errors (it just dies).
#
# Dependencies:
#
# - A relatively recent version of Bash.
# - identify(1), from ImageMagick 6.
# - img2sixel(1), from libsixel.
# - nproc(1), from coreutils.
#
# img2sixel(1) is used instead of ImageMagick's convert(1) because the
# conversion to Sixel is much faster with it.
#
# There are two environment variables that can be used to change the behaviour
# of the script:
#
# THUMBNAIL_SIZE Number of rows to determine the size of the thumbnail.
#
# BORDER_COLOR Color of the highlight border. It is expected in
# hexadecimal form (i.e., "FF0000" is red).
set -euo pipefail
: "${THUMBNAIL_SIZE:=5}"
: "${BORDER_COLOR:=FF7700}"
printf -v BORDER \
'\e[38;2;%d;%d;%dm' \
$(("0x${BORDER_COLOR:0:2}")) \
$(("0x${BORDER_COLOR:2:2}")) \
$(("0x${BORDER_COLOR:4:2}"))
# Terminal dimensions.
IFS=';' read -p $'\e[14t' -rs -dt _ WIN_HEIGHT WIN_WIDTH
IFS=';' read -p $'\e[18t' -rs -dt _ ROWS COLUMNS
CELL_WIDTH=$((WIN_WIDTH / COLUMNS))
CELL_HEIGHT=$((WIN_HEIGHT / ROWS))
IMAGE_WIDTH=$((CELL_WIDTH * THUMBNAIL_SIZE * 2))
IMAGE_HEIGHT=$((CELL_HEIGHT * THUMBNAIL_SIZE))
NPROC=$(nproc)
# Main program.
declare -a workers=()
declare -a tmpfiles=()
trap cleanup EXIT
cleanup() {
if [ "${#tmpfiles[@]}" -gt 0 ]
then
rm -f "${tmpfiles[@]}"
tmpfiles=()
fi
}
declare -i CURRENT_ROW_OFFSET_X="$WIN_WIDTH"
declare -i CURRENT_ROW_HEIGHT=0
# Reserve vertical space and save the cursor at the beginning.
start_row() {
if [ "$CURRENT_ROW_HEIGHT" -gt 0 ]
then
printf '\e8\e[%dB' $((CURRENT_ROW_HEIGHT / CELL_HEIGHT + 1))
fi
for (( n = 0 ; n <= THUMBNAIL_SIZE ; n += 1 ))
do
printf '\n'
done
printf '\e[%dA\e7' "$THUMBNAIL_SIZE"
CURRENT_ROW_OFFSET_X=0
CURRENT_ROW_HEIGHT=0
}
# Draw a single image from the workers array.
draw() {
if [ "${#workers[@]}" -eq 0 ]
then
return
fi
# Extract first worker from the array.
local worker="${workers[0]}"
workers=("${workers[@]:1}")
read -rs width height < "$worker.size"
local next_offset_x=$((CURRENT_ROW_OFFSET_X + width))
if [ "$next_offset_x" -ge "$WIN_WIDTH" ]
then
start_row
fi
if [ "${height}" -ge "$CURRENT_ROW_HEIGHT" ]
then
CURRENT_ROW_HEIGHT=${height}
fi
if [ "$CURRENT_ROW_OFFSET_X" -gt 0 ]
then
printf '\e8\e[%dC' $((CURRENT_ROW_OFFSET_X / CELL_WIDTH + 1))
fi
printf '%s\e]8;;%s\a' "$BORDER" "$(cat "$worker.image")"
cat "$worker"
printf '\e]8;;\e[m\a'
CURRENT_ROW_OFFSET_X+=$((width + CELL_WIDTH * 2))
}
for image in "$@"
do
if [ "${#workers[@]}" -ge "$NPROC" ]
then
# We have to wait for the process before calling draw() because the
# subshell created for the function can't wait for a process created
# in this scope.
wait "$(cat "${workers[0]}.pid")"
draw
fi
image=$(realpath "$image")
# The “worker” is a background shell where the image is rescaled
# and converted to Sixel.
worker=$(mktemp)
printf "%s" "$image" > "$worker.image"
(
identify -format '%w %h\n' "$image" > "$worker"
read -rs width height < "$worker"
if [ "$width" -gt "$height" ]
then
scale=(-w "$IMAGE_WIDTH" -h auto)
else
scale=(-h "$IMAGE_HEIGHT" -w auto)
fi
img2sixel \
"${scale[@]}" \
-r nearest \
-q low \
-o "$worker" \
"$image"
identify -format '%w %h\n' sixel:"$worker" > "$worker.size"
) &
printf "%s" "$!" > "$worker.pid"
workers+=("$worker")
tmpfiles+=("$worker" "$worker.image" "$worker.pid" "$worker.size")
done
# Show pending images.
wait
while [ "${#workers[@]}" -gt 0 ]
do
draw
done
echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment