Images in the Terminal Revisited

March 13, 2023

I’ve posted previously about terminal image viewing, with a particular emphasis on viewing image links in weechat. For the past few years I’ve been using an approach that doesn’t open them in the terminal at all, but I’ve continued using timg for fast image previews outside of weechat.

Both of these approaches have limitations, however; using timg (and similar programs like tiv) limits your resolution dramatically, and both the ssh and OSC approaches require setup on the client.

Conveniently, however, my preferred terminal emulator, Konsole, recently added support for three raster graphics protocols: Sixel, iTerm, and Kitty. This opens the door to displaying images in the terminal, at full resolution, with no requirement for client support beyond using a compatible terminal emulator.

When using timg outside tmux, this is very easy; just add -pk to enable Kitty mode and you’ll get a nice raster image in the terminal. In tmux, however, it doesn’t work because tmux doesn’t understand the Kitty protocol; the control sequences used for raster graphics are just discarded.

The solution, of course, is to get tmux out of the way entirely! It turns out that you can pass a continuation to tmux detach with -E, and if you end that with tmux attach it’ll reattach to the session, so a command like:

tmux detach -E 'timg -pk foo.png; sleep 5; tmux attach'

will drop out of tmux temporarily, display foo.png using the Kitty protocol, and then reattach to tmux.

This isn’t great UX; sometimes you want to view multiple images, or look at something for more (or less) than five seconds, etc. I ended up writing a wrapper script, timgallery, to address this; it uses timg for image rendering, but:

  • uses the alternate screen buffer so it doesn’t clobber your backscroll
  • displays one image at a time and lets you page between them
  • if passed http(s) URLs, automatically downloads the images in order to display them
  • if run inside tmux, automatically relaunches itself outside tmux

The “automatic relaunch” part is pretty straightforward and can be used in any shell script that needs to temporarily place itself outside of tmux:

if [[ $TMUX_PANE ]]; then
  local session="$(tmux display-message -p '#S')"
  exec tmux detach -E "$0 $@; tmux attach -t '$session'" || exit 1
fi

And for programs that don’t automatically do this, I can write a simple wrapper:

function without-tmux {
  if [[ $TMUX_PANE ]]; then
    local session="$(tmux display-message -p '#S')"
    tmux detach -E "$@; tmux attach -t '$session'"
  else
    "$@"
  fi
}

Which lets me do things like without-tmux catimg foo.png.

While working on this, there was one more upgrade I made to weechat-open-url, my weechat URL handling script: automatic content type detection. Earlier versions based the decision on whether to open as an image or a generic URL on the URL itself, but there are a lot of URLs out there that don’t look image-shaped but nonetheless serve images, and some that look like images but actually serve some kind of wrapper or landing page.

It turns out that you can easily probe the content-type using curl, so now it looks like:

content_type="$(curl -sLI -w "%{content_type}" -o /dev/null "$1")"
if [[ $content_type == image/* ]]; then
  open-image "$1"
else
  open-url "$1"
fi

where open-image and open-url are functions that invoke timgallery or the custom OSC protocol respectively.

And with that, I now have a much nicer chat experience; I can use the URL hinter to open anything that looks like it might plausibly be an image and view it directly in the terminal at full resolution, and non-images will be routed to my local browser as before!