Rendering the Web with Pictures in Your Terminal : more than you would like to know about HTML, ANSI and the philosophy of communication.

Message ID
DKIM signature
Download raw message
by Ploum on 2022-03-24


I’ve often been baffled by the productivity of Open Source developers. 
But I may have found the secret. Having something else to do. As soon as 
you need to do something urgently, something non-computer related, 
programming open source seems really important.

So, in a new episode of "I should really have done something else with 
my life", please welcome the "Offpunk got a new HTML rendering" story, a 
long meditation on reading HTML and starting meaningful, philosophical 

Discovering ANSI Codes

When I started using AV-98 as a Gemini client, I thought it was simple 
because it simply displayed the Gemtext content on the screen, only 
replacing URLs with numbers. I didn’t realise consciously that titles 
had to be coloured. But how can you send colours to a terminal? Isn’t 
that only white lines on black background ?

The answer is ANSI codes. ANSI codes are special characters that, when 
passed to a terminal, change the "mode" of the terminal. If you type 
"\e" followed by "[", the terminal understands it as the "Control 
Sequence Indicator". You can then type different numbers (separated by 
";") and end with "m". The different numbers correspond to different 
modes. So typing "\e [1m" switch to bold and "\e [1;35m" (without the 
space) switch to bold and magenta.  You can close all opened modes with 
"\e [0m".

It is limited to 8 background/foreground colours and a few other modes 
like bold, faint, italic, underline, …

It’s all fun until you realise that those 8 colours can be customised 
using another special code. So, in essence, your terminal can display 
any Unicode character with any background and foreground colour. (Not 
all terminals have the same colouring capabilities but, still…)

As I was discovering that through reading AV-98 code, I also wondered 
how hard it would be to display HTML pages in what was already called 
Offpunk. If I could display a gemtext page with blue title, how hard 
would it be to display some HTML? I would only need to transform HTML 
tags into ANSI codes. Right?

Why HTML is a Bad Way of Communicating

I hacked a first version displaying links in blue and, yes, it was 
working! This had been surprisingly easy! But the more I kept trying 
webpages, the more I realised how bad the situation was.

First of all, HTML is really hard to parse for a simple single reason: 
space and return characters may act completely differently depending on 
where they are in the page. In fact, except in <PRE>, one could say that 
you could drop returns. Except that, sometimes, they should be 
considered as a whitespace (if you don’t, some pages will 

But that’s only when the HTML is good. Most of the time, it isn’t. My 
initial HTML2ANSI routine started to grow organically to handle all the 
cases I was finding. It was try and error: add a space there, reload, 
see if it looks good, add another strip() there. No architecture, pure 
random code.

When looking at the source code, I found myself multiple times pondering 
if I should display what the people obviously intended to say or if 
I should display what the HTML told. One very small but obvious example: 
Wordpress websites using Jetpack have some of their picture duplicated. 
The reason is that one of the pictures is behind a "lazyloading" 
attribute while the other is in a "<noscript>" tag. Offpunk should 
obviously display the "<noscript>" because it doesn’t do JS but also 
displays the first one because it doesn’t parse the lazyloading CSS 
attribute. I don’t see how I can correctly not display the picture 

Tools are simply bad and produce awful HTML. If you are not convinced, 
look at the source of a page generated by the pseudo-minimalist Medium. 
When trying to parse HTML, it becomes obvious how we lost any meaning. 
People wrote, tools transformed it in a meaningless mumbo jumbo and 
I was trying to write a tool to find the original meaning back.

But tools are not the only ones to blame…

People are Bad at Writing

When I added support for bold and italic, something strange appeared. 
I realised the test pages I’ve been using since the beginning were now 
less readable. The text alone was good but people are somehow forced to 
add many fancy stuff over it. It’s like they fear not being remarked. 
Every HTML webpage looks like a child doing stupid stuff to attract his 
parent’s attention.

Open a book. Open 10 books around you. Try to find how many instances of 
bold sentences, varying styles and coloured words you have. Except in 
some very modern technical books, there’s none. You can sometimes find 
some italics to indicate a citation. It’s quite rare. I am, of course, 
referring to books that were published before writers started to use 
MS Word.

As Neal Stephenson said in "In the Beginnings was the command line", 
bold was at first only used for the title. Then MS Word came without any 
support for formal title/subtitle styling. The bold button was put 
prominently and people were expected to make their titles manually. But 
something else happened: people started to put bold and italics 
everywhere. Because they could.

When reading through so many webpages, it struck me how all this styling 
was looking like insecurity. Fear of not being read. Fear of not being 
cool. Fear of being boring. Fear of losing your audience. And how much 
better most of the texts were without any styling.

As I was reading "L’Âge des low-tech", by Philippe Bihouix, I came 
across one of those insecurities. The author put the word "NEVER" in all 
caps, probably hoping to make a point. It looks like the author felt 
insecure about his argument, felt that it was weak and tried to 
compensate. The point was something like "3D printing is not a solution 
because true constructions like houses can NEVER be 3D printed". Which 
is obviously false (or at least it should be explained why current 
3D-printed buildings are not trully 3D-printed). Without the all cap, it 
would have been an error in a book, something understandable. With the 
all cap, the author lost a lot of credibility for many pages.

Any Mistake That Can Be Made…

So, in essence, people are using crappy tools that produce shitty code 
and they are using it as badly as they can. We can safely say that any 
error that can be done will be done.

The same is true for Gemini. Hopefully, Gemtext is so simple that 
there’s no tool, reducing the error surface. Errors can only be done on 
a line-by-line basis. Like that case when someone was speaking about a 
hashtag and the hashtag appeared at the beginning on the line, turning 
the line into a title.

But there’s one true error still possible with gemtext : Preformatted.

I’ve seen them all : people missing one preformatted line and thus 
inverting formatted/preformatted, people not closing an opened 
preformatted or, the worse : some gemlogs switching between 
formatted/preformatted for each paragraph. The only explanation I found 
was that it was looking cool in their test client. What if their client 
was switching the background colour for preformatted? Then their posts 
would render as a nice patchwork of colours.

This is a huge problem as Offpunk doesn’t wrap preformatted texts, 
assuming they are some sort of ASCIIART. Those paragraphs were thus 
displayed as a long non-wrapped line.

If a mistake can be made, it will be made.

About Wrapping

It seems obvious when you are reading texts but, once a line is longer 
than your screen, that line should be wrapped. The line should continue 
on the next line. Wrapping is far from trivial if you don’t want to wrap 
in the middle of words. You need to count characters then, once a 
threshold has been reached, cut at the nearest word separation.

But remember what I told you about ANSI codes and colours. When you see 
a coloured line on a terminal, there are some invisible characters. Many 
of them. So the wrapping is confused and may wrap too soon.

If you have a Gemini title you want to be bold blue, the ANSI sequences 
to open and close everything will amount to 12 characters. Your wrap 
will be really short.

Not a huge problem until you start dealing with HTML lines where you can 
have 4 or 5 links per line. You may start to wrap before displaying any 
character at all!

ANSI modes should be conserved to the next line but I observed many 
singularities and, from what I’ve seen, it is a good practice to close 
all ANSI codes before the end of a line. That makes the wrap even 
harder: you need to identify all opened ANSI sequences, close them and 
reopen them on the next line.

That’s exactly the problem that python-ansiwrap is solving. And they did 
quite a good job with it.

Displaying Images

As I was reading webpages with Offpunk, I realised once that I was 
really missing an important information. I loaded the page in Firefox 
and, yes, a whole paragraph, a citation, was missing. What happened? It 
took me the source code to realise that the citation was not a text but 
a picture.

The easy solution to avoid such problem was to render pictures as links. 
If you wanted to see a picture, you would simply follow the link and the 
picture would be displayed in your picture viewer (feh, in my case).

But, quickly, I started to open all the pictures, curious to see what 
they were. Most pictures on the web are useless. They serve to get a 
sense of "feeling". Like bold and styling, they only serve to attract 
attention to the content. I’m guilty as everyone. For years, I tried to 
find a catchy picture for every article on my blog. This was reinforced 
by the fact that a link without a picture has no chance to thrive on 
Twitter nor Facebook. It even became some sort of artistic ritual to 
pair a finished blog post with a totally unrelated picture.

As I discovered the console, more than 20 years ago, I started to play 
with libcaca, which was transforming pictures into asciiart. I did a 
little experiment but, on the small size of a website, a libcaca picture 
does not help at all. It is fun but there’s no way you could recognise 

At that point, the universe told be about Chafa. For the anecdote, I was 
removing unused packages from my computer when I found one called 
neofetch. I did a "apt-cache show neofetch" to see what it was and was 
intrigued by a dependency called "chafa").

Remember that your terminal can display any Unicode character with a 
custom foreground and background colour? Well, chafa uses that to 
transform any picture into a configurable block of coloured text. What 
is really interesting is that most pictures are recognisable. I also 
found "timg", which is doing exactly the same as chafa albeit slower.

I thought it would be fun to have chafa rendering pictures in Offpunk 
and tried it. It was just a proof of concept. But I was quickly 
convinced. It was working so well that I could easily tell if a picture 
was an important one I should open or a decorative one I could skip. In 
a word, it was "useful". One more tool to extract information that 
people are deliberately burying while trying to promote it.

A webpage with pictures rendered in Offpunk. Notice how you can
recognize that IMG 11 is a meme while IMG 15 is a screenshot.

With the 1.8 version, Chafa added support for a kind of "kitty-image- 
protocol", something I don’t understand but which allows the Terminal 
Kitty to display pixel-perfect pictures. Timg already had support for 
that. Pixel perfect pictures cannot be integrated into text but this 
meant that I could directly display pictures into Offpunk. And, yes, it 
works when you access the picture directly.

Offpunk displaying a picture from a gemlog you should follow

For best result, use Kitty terminal and Chafa 1.10. If you have an older 
Chafa, also install Timg to have the best of both worlds.

Rewriting All From Scratch

All this hacked-together plate of spaghetti was working quite well but 
was unmaintainable. The HTML rendering code was pure random guessing. 
Ansiwrap sometimes gave very strange results and the performances were 
horrible. A 40 characters wide line produced by chafa could contain up 
to 1000 characters, which were parsed by ansiwrap.

When reading the Ansiwrap source code, I realised that one hard task was 
to first identify ANSI codes in a text. Something I could avoid as I was 
doing it myself.

So, in essence, I was making a text full of ANSI codes then wrapping it 
with ansiwrap. Could I do it the other way?

I started a new renderer that would save the position of every future 
ANSI code without inserting them. Then, I would wrap it, line by line, 
inserting ANSI codes only after wrapping while being smart enough to 
close them before the end of the line and reopening them.

Chafa pictures would be inserted without being wrapped at all.

This was not an easy task and I even added a BETA switch to keep the old 
rendering engine while I was working on the new. But, quickly, 
I realised it was doing a better job. Instead of transforming HTML to 
ANSI, I was first trying to understand HTML then building a meaningful 
representation. For example, newparagraph() and newline() are two 
different functions because they have a different meaning. For the 
anecdote, the first rendering engine was adding so many blank lines that 
I needed to remove every block of more than three blank lines before 
displaying the result.

So, starting with 1.2, Offpunk as a whole new rendering engine, also 
used for Gemini, Gopher and RSS. Please test your favourite capsules and 
websites and report any problems.

Download Offpunk

Send me an email about Offpunk

Subscribe to the offpunk mailing list to discuss

Using the renderer without Offpunk

An HTML2ANSI renderer seems very useful and I’m wondering if I should 
release it as a standalone tool. You can already test it quite easily 
with the following python script within the same folder as Offpunk.

!/usr/bin/env python3

from offpunk import HtmlRenderer

f = open("index.html")

content = f.read()


rend = HtmlRenderer(content,"www.test.com")

display the result in less


or put it in a variable

body = rend.get_body()


Making the Web Readable

I’m proud to say that my first objectives have been met: removing any 
dependency to python-ansiwrap, improving performance with pictures and 
making the code more maintainable.

Outside of ansiwrap, Offpunk uses two libraries to parse the web: the 
venerable BeautifulSoup 4 (python-bs4), which does an outstanding job of 
parsing the HTML. Parsing is the act of transforming a text into an 
usable computer representation. Rendering is the opposite : transforming 
the computer representation into something an user can read/watch. The 
best the parsing, the easiest the rendering.

The other is python-readability which removes unwanted cruft from an 
HTML page. Python-readability transforms HTML into HTML but with less 
thing. Most of the time, it works really well, allowing Offpunk to 
display only the article that I want to read.

Sometimes, it removes too much. In that case, Offpunk allows bypassing 
readability with the command "view full" (or "v full"). Starting with 
1.2, you can bookmark a page and its mode, so you automatically switch 
to "full" for some pages. This is done with a very naughty hack (adding 
"##offpunk_mode=full" at the end of the URL and crossing fingers for no 
URL having this string in their URL in real life).

Without python-readability, I could not have added web support to 
Offpunk. It would have been discouraging. But now that I have an 
improved HTML renderer, I’m starting to wonder if I could make python- 
readability an optional dependency. Would it be useful for anyone not to 
install python-readability?

There’s one good reason not to allow running Offpunk without python- 
readability. With readability enabled, most webpages have between 0 and 
20 or 30 links. When running "offpunk --sync", this means making that 
many requests to prefetch contents and pictures.

But in "full" mode, most webpages have between 100 and 500 links. Most 
of them completely useless, of course. Using Offpunk to surf the web 
without readability might be a nightmare.

Starting a Discussion

I created my first website in 1998 and started my still active  blog in 
2004. For all those years, I was publishing to attract attention, 
readership. I was looking for some "success". I got some. Published 
books, was invited to give conferences, was offered jobs, was even twice 
recognized by strangers because of my blog. But it was never enough. It 
is still not enough. I don’t feel particularly successful.

For the last 20 years, I’ve tried to become a successful writer by doing 
anything that could attract attention to my writing. In the last years, 
I’ve been slowly evolving toward writing more, publishing less and doing 
marketing even less.

By being forced to parse all the greasy cruft that others put around 
their writing, I was forced to realise how I did (and might still do) 
the same. How the appearance is detrimental to the content. Anyone 
suggesting that gemtext should support colours should be forced to write 
an ANSI parser first. Anyone suggesting that Gemtext formatting is not 
enough to express correctly what they want to express should open an old 
book. Most people are trying to work around the fact that writing is 
hard. Really hard. They hope to share emotions and non-verbal clues 
through emojis, colours, boldening. The problem is that, unlike the 
written language, those expressions are not explicit. You don’t convey 
what you want to convey, you make it less understandable. If you can’t 
write without colours and emojis, it is probably because you don’t 
really know what you want to say. That’s why writing is so hard. It is 
hard but it pays off. When writing with only words, you not only 
communicate better with others, you also communicate with your present 
and future self.

« Ce qui se conçoit bien s’énonce clairement et les mots pour le dire 
viennent aisément. » (Boileau)

I’m still astonished by the emptiness of most successful webpages once 
displayed in Offpunk. Offpunk made the front page of Hackernews which 
led me to break my disconnection temporarily because I was curious about 
other links with more votes. They were shallow, empty and rarely had 
more than 200 words. A search about Offpunk on Google that same day 
revealed that the top pages were mostly computer remixes of the Offpunk 
README. The web is full of automatic content trying to mimic what has 
been successful. There were even two Youtube videos in the first Google 
results. Both of which are computer reading of the README while the 
website is displayed. The real Offpunk website was only on the second 
page, demonstrating how the web is buried in shitty auto-generated 
content and how bad the leading search engine is.

I did not receive more emails that day than when I post on this gemlog.

That success everybody is running after is fake, short-lived. It is not 
even finished that you must already run for the next shot.

With Offpunk, I still can read people I like on the web. I follow their 
RSS feeds. It makes me feel like I’m still on Gemini or Gopher. It’s 
funny how, each time I find something deep and meaningful to read on the 
web, it renders well in Offpunk. It feels a bit like meaning and 
technical presentation are somehow connected.

I also like to write on Gemini because I know that it can’t be 
successful. I can write long stuff like this one. It will be read by 
people that find it interesting. Not many. Maybe none. But if you made 
it until now, then it certainly had an impact on you. I’ve shared my 
thoughts with you. You will, consciously or not, process them. You may 
even one day produce something that will have been impacted by what 
you’ve read today. I may read you later without knowing that.

This whole post has been influenced heavily by a lot of people in the 
Gemini space. They made me think. You probably made me think. I’ve 
evolved thanks to what I’ve read. Sometimes, when reading you, I wanted 
to reply. But there was no short and easy way to reply. So my reply 
either got lost, which means it was not important, or got carried with 
my thinking and crystallised into this post or the following one.

We have started a long, slow and meaningful conversation. The kind of 
conversation upon which every human breakthrough was conceived. Thank 
you for participating in it.

This is just the beginning of our conversation.
Reply to thread Export thread (mbox)