← XeFM crftwr/xefm on GitHub · craftware

Image Viewer

XeFM has a built-in image viewer. Put the cursor on an image and run view_file (or Enter) to open it full-window, with zoom, pan, and navigation to the other images in the same directory — without leaving XeFM.

Opening

Action What it does
view_file View the focused file — an image opens in the image viewer
open_item Same, for a file with no other enter rule

Recognized formats: PNG, JPEG, GIF, BMP, WebP, TIFF, ICO, TGA, and the netpbm family (PPM/PGM/PBM/PNM).

Controls

The viewer’s own footer names the keys, and F1 inside it lists them all.

Action What it does
image_viewer.zoom_in Zoom in
image_viewer.zoom_out Zoom out
image_viewer.zoom_reset Fit the whole image to the window
image_viewer.pan_* Pan (while zoomed in)
mouse drag Pan
mouse scroll Zoom in / out
image_viewer.next / .prev Next / previous image
Home / End First / last image
F1 Key help
quit / Esc Close

Zoom starts at fit — the whole image in the window — and each step magnifies by 25%, up to 40×. Panning past an edge stops at the border rather than dropping your zoom level, and zoom and pan reset when you move to another image.

The header shows the file name, its position in the list (2/7), pixel dimensions, file size, and the current zoom while you are zoomed in.

Prev / next navigation

↓ and ↑ walk the images in the pane you opened the viewer from, in the order shown there — so your sort order and filters carry into the viewer, and non-image files are skipped. The list wraps at both ends.

The pane’s cursor follows along: stepping to another image (including with Home/End) moves the file list’s cursor onto that file, so closing the viewer leaves you exactly on the image you were looking at.

The list is fixed when the viewer opens, so a background directory refresh cannot shift it under you. Close and reopen to pick up new files.

CRT / Pip-Boy themes

On a theme with a screen post-effect (glow, scanlines, bloom), the effect is suspended while an image is open so you see the real pixels rather than a filtered version of them. It returns automatically when you close the viewer.

Terminal support

Terminals do not all display images. XeFM uses whichever inline-image protocol your terminal speaks:

Terminal Protocol
kitty, Ghostty, WezTerm, Konsole kitty graphics
iTerm2, mintty iTerm2 inline images
foot, contour, mlterm, xterm (-ti vt340) sixel

In the desktop app images always render, whatever your terminal.

In a terminal without any of these protocols — Terminal.app and the VS Code terminal, notably — the viewer shows a card with the format, dimensions and file size instead of the picture. Navigation still works; zoom and pan are hidden, since there is nothing to zoom. To view the picture itself, use a terminal from the table above, run the desktop app, or use open_with_os to hand the file to your OS image viewer.

You can force or disable the protocol with the PUIKIT_TERM_GRAPHICS environment variable (kitty, iterm2, sixel, or none) — useful when XeFM guesses wrong, or inside tmux/screen, which intercept these sequences:

PUIKIT_TERM_GRAPHICS=none xefm     # never try to draw images inline
PUIKIT_TERM_GRAPHICS=sixel xefm    # force sixel

Formats

PNG, JPEG, GIF, BMP, WebP, TIFF, ICO, TGA and the PNM family, plus AVIF, JPEG 2000, PSD, QOI, ICNS, DDS, CUR, APNG and PCX. Those all come from Pillow, which ships with XeFM, so they work the same on every platform.

HEIC, JPEG XL and camera RAW depend on your machine, because XeFM asks the system rather than carrying a decoder for them:

  macOS Windows Linux
HEIC / HEIF yes, built in with HEIF Image Extensions from the Store — plus HEVC Video Extensions, which decodes what a HEIC actually holds pip install pillow-heif
AVIF yes yes yes
JPEG XL yes, built in if your Windows has the codec — recent builds do pip install pillow-jxl-plugin
Camera RAW (DNG, CR2, NEF, ARW…) see below see below see below

On macOS the pictures are drawn by the same decoder Preview and Quick Look use; on Windows by whichever imaging codecs are installed. So a HEIC off your phone opens with nothing to set up on a Mac, and on a Windows machine with the Store extensions installed. On Linux, one pip install is the whole of it.

These are per machine, not per version of XeFM. The desktop app and the terminal app answer alike: run XeFM in a terminal on a Mac and a HEIC still opens, drawn by the system decoder and handed to your terminal’s inline-image protocol. (A terminal that cannot show pictures at all is a separate matter — see Terminal support above.)

The Windows column is not a version list. What XeFM offers there is whichever WIC imaging codecs the machine actually has, which depends on the Windows build and on which Store extensions are installed — so two machines running the same XeFM can honestly differ. XeFM asks at startup rather than assuming, which is why this table can only tell you where to look. (Measured on one Windows 11 build 26200 machine: fourteen decoders, 65 extensions, JPEG XL among them with no Store package installed for it.)

XeFM only ever offers a format something on your machine can read. If a .heic opens in the image viewer, you get a picture — never a “cannot show this” card. On a machine with no HEIC decoder the file simply is not treated as an image, and V falls back to whatever else you have set up for it (see Opening in an external viewer instead, below).

Adding a format yourself

Camera RAW, SVG, DICOM and anything else are a few lines in your config. Write a function that turns the file’s bytes into a picture and name the suffix:

import io

def open_raw(data):
    import rawpy                      # pip install rawpy
    from PIL import Image
    with rawpy.imread(io.BytesIO(data)) as raw:
        return Image.fromarray(raw.postprocess())

class Config:
    IMAGE_DECODERS = {
        '.dng': open_raw,
        '.cr2': open_raw,
        '.nef': open_raw,
    }

Registering a suffix is also what makes the viewer offer that format, so Enter on a .dng now opens the picture. Your function gets the file’s bytes — not a path — so the same decoder works on a picture inside a zip or on an S3 bucket. Naming a suffix XeFM already reads replaces its decoder with yours.

Full details in doc/CUSTOMIZATION_FEATURE.md; your config file has a worked example in its IMAGE_DECODERS section.

Requirements

Pillow ships with XeFM and decodes most of the list above. If it is somehow missing, the viewer still opens and still navigates, but shows the metadata card instead of the picture.

Remote and archived images

Images on S3 or over SSH, and images inside an archive, open like any other, and nothing is written to disk on the way — their bytes go straight to the decoder. (The one exception is a format only your operating system can read, such as a HEIC inside a zip on a machine with no pillow-heif: that one is staged in a temporary file for the life of the viewer and removed when it closes.)

Opening in an external viewer instead

By default view_file uses the built-in viewer and open_with_os hands the file to your OS app. To send V to an external program too, point the view entry at it in your config’s FILE_ASSOCIATIONS:

FILE_ASSOCIATIONS = [
    {
        'pattern': ['*.jpg', '*.jpeg', '*.png', '*.gif'],
        'open|view': ['open', '-a', 'Preview'],   # V leaves XeFM again
        'edit': ['open', '-a', 'GIMP'],
    },
]

Setting 'view': None (the default) selects the built-in viewer.

Upgrading: XeFM’s shipped default used to send view to Preview. Your existing config keeps whatever it has — change that entry as above if V still opens Preview and you would rather stay in XeFM.

Customizing keys

The zoom, navigation, and pan keys are rebindable in your config’s KEY_BINDINGS:

KEY_BINDINGS = {
    'image_viewer.zoom_in':      ['+', '='],
    'image_viewer.zoom_out':     ['-', '_'],
    'image_viewer.zoom_reset':   ['0'],
    'image_viewer.next':         ['DOWN'],
    'image_viewer.prev':         ['UP'],
    'image_viewer.pan_up':    ['Shift-UP'],
    'image_viewer.pan_down':  ['Shift-DOWN'],
    'image_viewer.pan_left':  ['Shift-LEFT'],
    'image_viewer.pan_right': ['Shift-RIGHT'],
}

Home/End are viewer-local and not rebindable, matching the text viewer’s scroll keys.

If your config predates these actions (they are missing from your KEY_BINDINGS), the viewer keeps its historical keys: n / p step through the images and the plain arrow keys pan. Add the entries above to switch to the current defaults.

See also