^ ^ ^ ^ =(o.o)= ~byakuren =(o.o)= m m m m
This patch adds native ASCII art image rendering to w3m, enabling inline image display in terminals without framebuffer, sixel, or proprietary graphics protocol support. Images are converted to colored ASCII/ANSI text and rendered directly in the terminal, making them viewable over SSH sessions, TTY environments, terminal multiplexers, and any text-based display.
Here’s what w3m looks like rendering images as ASCII art in my text only terminal, using this configuration:
inline_img_protocol 5
asciiart_display chafa --size=%dx%d --stretch --colors=256 \
--symbols=space+solid+stipple+block+border+diagonal+dot+quad+half+hhalf+vhalf+braille+technical+geometric+ascii+wide \
'%s' 2>/dev/null
USE_IMAGE supportcd /path/to/w3m
patch -p1 < w3m-ascii-art.patch
./configure
make
make installAdd to ~/.w3m/config:
inline_img_protocol 5
asciiart_display chafa --size=%dx%d --colors=256 --symbols=block --stretch '%s' 2>/dev/null
Or use the options menu (o key) to select “ASCII art”
for the inline image protocol.
The asciiart_display command uses printf-style
placeholders:
| Placeholder | Description |
|---|---|
%d (first) |
Width in characters |
%d (second) |
Height in characters |
%s |
Image file path |
Important: Redirect stderr to /dev/null
to avoid display interference.
High Quality (Unicode blocks with 256 colors):
asciiart_display chafa --size=%dx%d --colors=256 --symbols=block --stretch '%s' 2>/dev/null
Classic ASCII only:
asciiart_display chafa --size=%dx%d --colors=256 --symbols=ascii --stretch '%s' 2>/dev/null
Monochrome ASCII (jp2a):
asciiart_display jp2a --width=%d --height=%d '%s' 2>/dev/null
Colored ASCII (jp2a):
asciiart_display jp2a --width=%d --height=%d --colors '%s' 2>/dev/null
Retro Style (libcaca):
asciiart_display img2txt -W %d -H %d '%s' 2>/dev/null
Once configured, images automatically render as ASCII art when browsing. No manual intervention required.
| Key | Action |
|---|---|
I |
Restart image loading and display |
ESC I |
Stop image loading |
o |
Open options menu (toggle display_image) |
h,j,k,l |
Vi-style navigation for large images |
<,> |
Horizontal scrolling |
# View local image
w3m file:///path/to/image.jpg
# View HTML with images
w3m https://example.com/gallery.html
# Test inline rendering
echo '<img src="test.jpg" width="80" height="40">' | w3m -T text/htmlThe implementation spans 10 files with ~950 lines of changes:
display.c - Draw condition updates for ASCII protocol
file.c - ALT text suppression and dimension scaling
fm.h - Protocol constant and ImageCache extensions
image.c - Parallel download/conversion with worker pool
main.c - Main loop polling for ASCII mode
rc.c - Configuration parsing for new options
terms.c - ASCII art rendering and ANSI filtering
terms.h - Function declarations
A new inline image protocol constant enables ASCII art mode:
// fm.h
#define INLINE_IMG_ASCII 5This integrates with existing protocols (0-4) and is selected via
inline_img_protocol configuration.
The ImageCache structure gains two new fields for ASCII
conversion tracking:
typedef struct _imageCache {
// ... existing fields ...
char *ascii_file; /* Path to cached ASCII art output */
char ascii_loaded; /* ASCII conversion status */
} ImageCache;Image downloads and conversions use a parallel worker pool:
static int getCpuCores(void)
{
int cores = 1;
#ifdef _SC_NPROCESSORS_ONLN
long result = sysconf(_SC_NPROCESSORS_ONLN);
if (result > 0)
cores = (int)result;
#else
// Fallback: parse /proc/cpuinfo
#endif
return cores;
}Workers are set to cpu_cores / 2 (minimum 1, maximum 8)
for optimal throughput without overwhelming the system.
Images are scaled to fit terminal dimensions while preserving usability:
void calculate_ascii_dimensions(int w, int h, int *full_w, int *full_h)
{
int max_width = COLS;
int max_height = LINES;
if (w > max_width || h > max_height) {
double scale_w = (double)max_width / w;
double scale_h = (double)max_height / h;
double scale = (scale_w < scale_h) ? scale_w : scale_h;
*full_w = (int)(w * scale);
*full_h = (int)(h * scale);
} else {
*full_w = w;
*full_h = h;
}
}Generator output is filtered to remove problematic terminal control sequences:
static int is_ansi_sequence(const char *s, int *length)
{
if (s[0] != '\033') return 0;
if (s[1] == '[') {
// CSI sequence: ESC [ ... <terminator>
// Terminators: 0x40-0x7E (@ through ~)
int i = 2;
while (s[i] && !((s[i] >= 0x40 && s[i] <= 0x7E)))
i++;
if (s[i]) {
*length = i + 1;
return 1;
}
}
return 0;
}This handles: - Color codes (ESC[...m) - Cursor
positioning (ESC[...H) - Cursor visibility
(ESC[?25h, ESC[?25l) - All CSI sequences with
terminators in the 0x40-0x7E range
┌─────────────────────────────────────────────────────────────────┐
│ 1. HTML parsed, <img> tags identified with dimensions │
├─────────────────────────────────────────────────────────────────┤
│ 2. ImageCache created with ascii_file temp path │
├─────────────────────────────────────────────────────────────────┤
│ 3. Worker pool downloads remote images in parallel │
├─────────────────────────────────────────────────────────────────┤
│ 4. On download complete: fork() to run ASCII generator │
│ - Command: asciiart_display %d %d '%s' > temp_file │
├─────────────────────────────────────────────────────────────────┤
│ 5. Conversion status polled, ascii_loaded flag set │
├─────────────────────────────────────────────────────────────────┤
│ 6. When ALL images complete: single display refresh │
├─────────────────────────────────────────────────────────────────┤
│ 7. put_image_ascii() renders output with ANSI filtering │
│ - Positions cursor at image location │
│ - Reads cached file line by line │
│ - Filters control sequences, preserves colors │
│ - Clips to viewport bounds │
└─────────────────────────────────────────────────────────────────┘
To prevent flickering with multiple images, the display only refreshes once all images are complete:
// Check if ALL images are complete
all_complete = 1;
for (j = 0, a = al->anchors; j < al->nanchor; j++, a++) {
if (a->image && a->image->cache) {
cache = a->image->cache;
if (cache->loaded == IMG_FLAG_UNLOADED)
all_complete = 0;
if (cache->loaded == IMG_FLAG_LOADED &&
cache->ascii_loaded != IMG_FLAG_LOADED)
all_complete = 0;
}
}
if (all_complete) {
displayBuffer(image_buffer, B_REDRAW_IMAGE);
image_buffer->image_loaded = TRUE;
}Large images with dense ANSI sequences required buffer expansion:
#define ASCII_LINE_BUF_SIZE 65536 /* 64KB for long lines with ANSI codes */A typical 256-color block character with positioning can be 20+ bytes per character, requiring substantial buffer space for wide images.
| Feature | ASCII Art (5) | External (0) | Sixel (2) | Kitty (4) |
|---|---|---|---|---|
| External process | Generator only | w3mimgdisplay | img2sixel | ImageMagick |
| Terminal support | Universal | X11/Framebuffer | Sixel-capable | Kitty only |
| Output type | Text characters | Pixels | Pixels | Pixels |
| Copyable | Yes | No | No | No |
| ALT text | Suppressed | Shown | Shown | Shown |
asciiart_display is configuredwhich chafachafa --size=80x24 test.jpgTERM=xterm-256color)For best results:
xterm-256color)--symbols=block --stretchThis feature was implemented across three commits:
This patch is released under the same license as w3m (MIT-like).