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.
| 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).
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.
↓ 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.
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.
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
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).
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.
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.
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.)
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
viewto Preview. Your existing config keeps whatever it has — change that entry as above ifVstill opens Preview and you would rather stay in XeFM.
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.
doc/dev/IMAGE_VIEWER_IMPLEMENTATION.md — how it works internallydoc/CUSTOMIZATION_FEATURE.md — IMAGE_DECODERS among the other things a
config can definedoc/TEXT_VIEWER_FEATURE.md — the viewer for everything that is not an image