Sometime in the past decade, the web took a wrong turn and has been too embarrassed to turn back.
Websites changed from being a medium to host static documents into rich, interactive software that doesn't require installation on your computer. With that complexity came a reduction of browser engines and a race to take two steps backwards, one step forward in the replacement of desktop software. If this resonates with you and you still primarily access the internet for the consumption of text-heavy content, this article will describe the state of pure text-based command line browsers in 2024.
In short, you can practically use text-based browsers in this day and age (I have been regularly doing so myself), so long as you only use them on traditional websites where your main focus is on text content. The moment you want to access a website with lots of images (if you want to look at something graphical), video, or interactive, it's not an option. So if you're reading a news article, searching Wikipedia, reading documentation, browsing a forum, or checking the phone number of a local business website, go for it. If you want to search for videos, do an image search, or access a graphical or interactive app of any kind, use a graphical browser. If you've gone this far, you'll start to be pleasantly surprised by the number of websites that work and offer quite a charming experience. You'll become quite skilled at learning to disregard the boilerplate and unneccessary cruft that plagues modern websites. Unfortunately, you will also be unpleasantly surprised by websites that you expect to be suitably rendered in text (browsing Github repositories and searching issues, visiting a Discourse forum to read posts, or checking out comments on Reddit, etc) only for the experience to be horrific or completely broken. Shame on these web developers.
The most advanced command line browser is elinks. I highly advise you also try out Lynx (the oldest), w3m (images in the terminal, cool!), and links (grandfather of elinks, faster, but much less features like colours, scripting, options, protocols, etc), and when you've convinced yourself that elinks is truly the only option for day-to-day usage, keep reading. If you come across stuff like felinks (later renamed to elinks) or links2 (fork with graphical mode), they're all dead and elinks is now the fork that matters.
Let's see what it looks like in action. Here's my very own website, showing a list of articles. Notice the numbers next to each link. This lets me quickly jump to a link.
Here's the OSArch forums. Notice the search input field on the top left. Notice how cleanly formatted this webpage is. It uses HTML rewriting with Python to optimise the layout, explained further below:
And here's the NetHackWiki, showing an article about a Giant Ant. In this case, link numbering is turned off, and there is nothing but content to focus on:
Here is the same page but showing ncurses menus for a more graphical experience, tables, and images in red (which if you click on them, will open in your image viewer, feh on my machine).
Features
- It's still maintained and developed.
- It has tabs. You can create tabs. Reorder tabs. Open links in new tabs. Close tabs.
- You can configure colours and styling.
- It understands a little bit of CSS styling.
- For long-form textarea authoring, you can launch your own text editor.
- It has bookmarks, history, and download manager.
- It has that annoying feature to "remember what I typed into this form"
- You can customise external apps to launch for different mimetypes (e.g. image viewer, video player, etc)
- It has mouse support to "click" on a link or scroll.
- You can "find in page"
- You can navigate with vi-keys (once mapped)
- Can be handled as a remote session, so when launching a link from another app (e.g mutt, newsboat, etc) it'll open it as a new tab in an existing elinks session
- Closing and reopening remembers the tabs from your last session
- It supports URL rewriting, such that typing "ddg foobar" auto searches DuckDuckGo (Lite) with the keyword foobar. Do need to type out full URLs.
- It supports HTML rewriting. Is your favourite website coded by a web developer who doesn't understand the importance of semantic HTML? You can pattern match URLs and auto manipulate HTML output to fix their broken website, client-side. Scary.
- It can be scripted with a variety of bindings, including Python and ECMAScript.
- You can visits websites using Gopher and Gemini
- If you do come across a website that is so utterly broken, you're a hotkey away from opening it in a more "modern" browser.
The best place to familiarise yourself with elinks is to read the Elinks documentation, as well as Thomas Adam's tips and Calmar's tips.
I've tweaked my elinks to have Vi-key bindings, support external browser, and view images, videos, and SVGs. This guide assumes you use a similar configuration to mine (otherwise, hotkeys and such will differ). At the bottom of this article, you'll see my ELinks configuration.
Navigation and browsing
Use o
to go to a new website in the current tab, or t
to open a new tab. URL completion is awesome, so get familiar with "URI rewriting". When you press o
, type in ddg thinkmoult
to search for "thinkmoult" on DuckDuckGo.
If you frequent sites, type s
to launch your bookmarks, then S
to search your bookmarks. You can quickly clear the form by doing <Esc>l
to clear, then press g
to visit the bookmark.
To navigate via single row/column scrolling, use hjkl vi-keys. Very often, you'll want to scroll multiple lines at a time, so use JK for this. HL is reserved to quickly go forwards and backwards in history.
Alternatively, use <Up>
and <Down>
to jump to the next link. The "active" link will be highlighted. Press <Enter>
to visit the link. You can also use O
to open that link in a new tab. You can use c
to close a tab. To undo a closed tab, press u
.
Instead of cycling manually through links, in practice you will use .
to toggle link numbering. Type the number to jump to the link. Toggling link numbering decreases readability of text, so it's better to toggle it than leave it permanently on.
<Up>
and <Down>
will also jump to form fields. Use <Enter>
to enter text into an input field. If it's a large text-area, use Ctrl-t
to launch vim to edit it.
On a long page where you want to jump between multiple points in the page, you can set "marks" to remember your position and jump between them. Type ma
to set a mark called a
, then use 'a
to jump to that mark. Once you go to a page you like, press a
to add it to your bookmarks.
When you inevitably reach a broken webpage, press E
and select the firefox
option to open the current page in Firefox. Send an angrily worded email to the webmaster.
Since elinks supports the mouse, you can also use the mouse to scroll, click on links, and even right click to open things in new tabs, and if you press <Esc>
you can navigate through the toolbar menus.
Styles and colours
Websites store information about how their content should be represented using CSS. This defines colours, spacing, fonts, and so on. Nowadays because developers sometimes don't know when to stop, they also define interactivity, animations, and content itself.
I'm strongly of the opinion that the web should've been designed in manner where the remote server was only capable of delivering semantic HTML, and the concept of how things should look like (i.e. the CSS) should be purely client driven. With elinks, you can somewhat experience this blissful state of affairs.
Elinks supports a non-CSS styled colour display: this is where all document (i.e. website) colours are determined by you. You don't have a lot of choice, given it's just text, but your elinks.conf
can set document.colors.*
to set things like the background colour, image colour, link colours, link number colours, text colours, and that's about it. Despite its limitation, it forms a great fallback when the website just has terrible colour choices and you just need things to be readable.
Alternatively, you can use CSS to style the website. Elinks will download and parse the CSS provided by the website. However, Elinks can also load a local CSS file that overrides the server-side CSS. Nice! Here's the contents of my default.css
:
body { background-color: #1a1a1a; color: #ddd; }
ul, ol { background-color: #1f1f1f; }
h1, h2, h3, h4, h5, h6 { color: #96f06e; }
a { color: #6ec8fa; font-weight: bold; }
code { color: #ff9900; background-color: #000; }
pre { color: #fff; background-color: #000; }
Notice I don't use !important
. This is because I use HTML rewriting to first strip all existing styling from webpages. I'm not a fan of server-side styling, I much prefer client-side styling. See the Python scripting section below.
Elinks offers the %
hotkey to toggle between three style modes:
- Non-CSS styles, using colours from
document.colors.*
- CSS styles, including local CSS
- CSS styles, including local CSS, but this time ignoring background CSS colours, since this is a very common source of unreadable text.
JavaScript / EMCAScript
There is theoretical support. This alas is just theoretical and basically expect websites that rely on JavaScript to break.
Hooks and HTML rewriting with Python scripting
This is an advanced, but very powerful feature. Simply create a hooks.py
(or hooks.lua
or hooks.js
) in ~/.config/elinks/
. This allows you to hook into events and override the behaviour. For example, the pre_format_html_hook
will take HTML and let you pre-process it before rendering it. If a website you frequent has garbage HTML, you can fix it.
Here's an example of a Github issue page. On the left is the original with only basic styling. Notice the long and inefficient block li elements used in the navigation, as well as feedback popup dialog and "loading" invisible div which is almost always irrelevant. On the right is the same issue page with HTML modifications which make it much nicer to browse (generally rendering list items with links inline are almost always an improvement in text browsers - notice the emojis representing reactions rendered nicely in a line).
Python has a neat library called BeautifulSoup which makes HTML manipulation really easy. Imagine if you could write a quick config file that takes a tag selector query (e.g. table.itemlist
) and auto apply some "cleaning" function from a library of cleaning functions to make it more readable. Here's an example which strips all styling, makes textareas a consistent size, makes tables actually look like tables, and so on.
Another neat thing this script does is to selectively allow the <link>
element. The HTML <link>
element is a way for documents to show that another page is related, such as for an alternative view of the page (like an Atom feed or RSS feed), or the same article in another language, or a next / previous page of a series of articles. Most graphical browsers hide this information from you. The document.html.link_display
option will show these links, and HTML rewriting can isolate only the most useful links. This is great if you're a heavy user of Atom feeds.
import elinks
import hooks
import rules
from bs4 import BeautifulSoup
from importlib import reload
def pre_format_html_hook(url, html):
# No need to restart elinks to test your rules
reload(rules)
if not url.startswith("http"):
return html
soup = BeautifulSoup(html, 'html.parser')
# Purge all unsemantic stuff. My CSS, my rules.
[e.decompose() for e in soup.find_all("template")]
[e.decompose() for e in soup.find_all("style")]
[e.decompose() for e in soup.find_all("script")]
[e.decompose() for e in soup.select("*[hidden]")]
for e in soup.descendants:
try:
del e["style"]
except:
pass
# I only like some semantic links
allowed_links = {"next", "prev", "license", "alternate", "author", "canonical", "privacy-policy", "terms-of-service"}
for e in soup.find_all("link"):
if "rel" in e.attrs and allowed_links & set(e["rel"]):
continue
e.name = "nolink"
# Textareas are meant to be big.
for e in soup.find_all("textarea"):
e["rows"] = 5
e["cols"] = 80
# Tables should look like tables with clear borders
for e in soup.select("table"):
e["border"] = 1
e["frame"] = "box"
# Very often websites have huge navigation lists or similar.
# This takes most lists and renders them inline. It's mostly an improvement.
for e in soup.select("li"):
e_strings = set(e.strings)
a_strings = set()
for a in e.find_all("a"):
a_strings.update(a.strings)
if e_strings == a_strings:
e.name = "span"
e.insert(0, soup.new_string(" • "))
return str(soup)
Check out this great article which provides a neat framework that does this. It makes "fixing" nasty HTML a breeze.
Elinks configuration
In .config/elinks/elinks.conf
, feel free to use this as a starting point.
# Vi-like navigation
bind "main" "h" = "scroll-left"
bind "main" "j" = "scroll-down"
bind "main" "k" = "scroll-up"
bind "main" "l" = "scroll-right"
bind "main" "H" = "history-move-back"
bind "main" "J" = "move-half-page-down"
bind "main" "K" = "move-half-page-up"
bind "main" "L" = "history-move-forward"
bind "main" "Ctrl-d" = "move-half-page-down"
bind "main" "Ctrl-u" = "move-half-page-up"
# Visit a new website (it'll pop up a fresh dialog to enter in a URL)
bind "main" "o" = "goto-url"
# Edit the current URL (it'll pop up a dialog with the existing URL)
bind "main" "e" = "goto-url-current"
# Reload webpage
bind "main" "r" = "reload"
# When a link is active, you can open it in a tab (or in the background)
bind "main" "O" = "open-link-in-new-tab"
bind "main" "T" = "open-link-in-new-tab-in-background"
# I never got this to work. Use the default < and > to change tabs.
bind "main" "Ctrl-Tab" = "tab-next"
bind "main" "Shift-Ctrl-Tab" = "tab-prev"
# Use [] to reorder tabs
bind "main" "]" = "tab-move-right"
bind "main" "[" = "tab-move-left"
# c to close tabs
bind "main" "c" = "tab-close"
# ?
bind "main" "u" = " *scripting-function*"
# Retain my current hand-written config, and only make changes as necessary if
# you change things through the UI.
set config.comments = 0
set config.saving_style = 3
set config.saving_style_w = 1
# set document.browse.links.numbering = 1
set document.colors.background = "#1a1a1a" # Close to black
set document.colors.image = "#fa6982" # Pink
set document.colors.link = "#6ec8fa" # Blue
set document.colors.link_number = "#dc6ea5" # Faded pink
set document.colors.text = "#eeeeee"
set document.colors.use_document_colors = 2
set document.colors.use_link_number_color = 1
set document.colors.vlink = "#117ebb"
# CSS is nice. My CSS is nice. Your CSS is not nice.
set document.css.enable = 1
set document.css.import = 0
set document.css.stylesheet = "default.css"
set document.html.link_display = 5
set document.html.wrap_nbsp = 0
set document.plain.compress_empty_lines = 1
# Do something with the current website in an external app.
bind "main" "E" = "tab-external-command"
# The "firefox" option will open the current page in firefox.
set document.uri_passing.firefox.command = "firefox-bin %c"
# The "xclip" option will copy the current URL to the clipboard.
set document.uri_passing.xclip.command = "echo %c | xclip -selection c"
# Just give up on JavaScript.
# set ecmascript.enable = 0
# Enable dumb and smart URI rewriting (so "ddg foo" searches DuckDuckGo for foo)
set protocol.rewrite.enable-dumb = 1
set protocol.rewrite.enable-smart = 1
# These colours are a lot more readable
set terminal.xterm-256color.block_cursor = 1
set terminal.xterm-256color.colors = 4
set terminal.xterm-256color.italic = 1
set terminal.xterm-256color.m11_hack = 1
set terminal.xterm-256color.strike = 1
set terminal.xterm-256color.transparency = 1
set terminal.xterm-256color.type = 1
set terminal.xterm-256color.underline = 1
set terminal.xterm-256color.utf_8_io = 1
set ui.colors.color.desktop.background = "black"
set ui.colors.color.desktop.text = "red"
set ui.colors.color.mainmenu.normal.background = "white"
set ui.colors.color.status.status-bar.background = "#96f06e"
set ui.colors.color.status.status-text.background = "#96f06e"
set ui.colors.color.tabs.normal.background = "#333333"
set ui.colors.color.tabs.normal.text = "#eeeeee"
set ui.colors.color.tabs.selected.background = "#96f06e"
set ui.colors.color.tabs.selected.text = "black"
set ui.colors.color.tabs.separator.background = "#222222"
set ui.colors.color.tabs.separator.text = "#333333"
set ui.colors.color.title.title-bar.background = "black"
set ui.colors.color.title.title-bar.text = "black"
set ui.colors.color.title.title-text.background = "black"
set ui.colors.color.title.title-text.text = "#dc6ea5"
set ui.language = "System"
# Save tabs when quitting and restore when starting
set ui.sessions.auto_restore = 1
set ui.sessions.auto_save = 1
# Immediately jump to redirect links
set document.browse.show_refresh_link = 0
set document.browse.minimum_refresh_time = 0
# Define custom handlers (image, video, pdf, svg) for extensions
set mime.handler.image.unix-xwin.program = "feh %u"
set mime.handler.video.unix-xwin.program = "mplayer -loop 0 %u"
set mime.handler.pdf.unix-xwin.program = "wget -O /home/dion/tmp/tmp.pdf %u && mupdf /home/dion/tmp/tmp.pdf"
set mime.handler.svg.unix-xwin.program = "firefox-bin %u"
# Define extensions we will handle
set mime.extension.webm = "video/webm"
set mime.type.video.webm = "video"
set mime.extension.mp4 = "video/mp4"
set mime.type.video.mp4 = "video"
set mime.type.image.jpg = "image"
set mime.type.image.jpeg = "image"
set mime.type.image.png = "image"
set mime.extension.pdf = "application/pdf"
set mime.type.application.pdf = "pdf"
set mime.extension.svg = "image/svg+xml"
set mime.type.image.xml+svg = "svg"
set mime.type.image.svg+xml = "svg"
# Download to this folder
set document.download.directory = "/home/dion/tmp/"