links2
We all know the three: Lynx, Links 2, ELinks. And w3m, the thing that pioneered inline images. Of these, Links is also among Dillo and NetSurf as a graphical browser —
Unlike its Links pre-1 fork, ELinks, which never chose to follow the upstream on that one;
Nowadays, elinks.cz web site is defunct for a month now — but exclusively on GitHub, the community remains active. Once a fork, rkd77’s ELinks, once (until 2020) having to call itself felinks, is now the continuation. The INSTALL mentions Alternatively, instead of Meson: ./configure && … …There is no ./configure. After I got all the deps one by one, the meson compile then told me my openssl/ssl.h expects a non-const CRYPTO_EX_DATA pointer as from parameter to socket_SSL_ex_data_dup, not const (which seems to be possibly a too lax type signature for today’s standards) — and by the way, clangd then told me, with my LibreSSL setup with preprocessor having no defs for the file in my editor:In 2022, @drizzlebactin has inquired in a GitHub issue, if there’s any plan for adding support for libressl. She received a No.
(Although this issue is probably not related to the compile error. And pretty sure not related to what I saw in my editor.)
This ELinks nowadays has «tabs, local cgi, mailcap, gopher», «is highly customizable and can be extended via scripts». The Gemini protocol support didn’t TOFU… so they SSL_VERIFY_NONE “for the short-term”. «FSP, gopher, bittorrent; browser scripting (lua, python, perl , etc.) […]»
@IngaLovinde once, last day of November 2024, thoroughly reported on findings including lack of display:none and visibility:hidden handling, which ELinks did in neither of its two (built-in, and LibCSS) CSS engines;
; within 25 minutes, ae got a community reply suggesting to try document.css.ignore_display_none = 0 (as = 1 means “show” for that setting), which helped some of the cases
— and the next morning some of aer findings were already receiving fixes from rkd77.
A bit oddly, I’m going to be interested in the graphical Links 2.
If we rank the three graphical browsers by how faithful they are to how illegible (small, etc.) the text seems to be mandated to be,
NetSurf will show overlapping pieces of text when your CSS is funky enough, and will make the text small if told to
Dillo will usually not enact the styling that chances it to overlap things, and the small thing to trouble you will usually be its own UI on a HiDPI display
Links2 will apply just one kind of CSS styling: display:none. You still have tables, you have the frames in your framesets, but the styling is dismissed. For the non-essential presentation aspects, that’s less than ELinks while being a graphical browser (in the graphical mode).
(Funnily enough, the logic for display:none in Links skips the hidden input tags, in order to not accidentally make them visible.)
When I’m already at HiDPI, I will mention that the changelog for release 2.19 mentions on a Windows case:
Sun Mar 31 15:59:40 CEST 2019 mikulas Disable high-DPI scaling on Windows Links makes it possible to specify scaling of text and images in the dialog windows, so this should preferably be used instead of system-level scaling
That is true and only slightly cumbersome, as the fonts for UI and for HTML are in two different whole menus. And you need to remember to “Save Settings” in the “Settings” menu. And “Save HTML Settings” in the “View” menu.
What really contributes to the aesthetic of graphical mode of Links to me is that while it can support FreeType fonts (at least can be compiled with such), by default it uses fonts that it pre-renders from PostScript font to bitmaps for the given size. And, optionally, subpixel optimization (which I keep disabled, staying on “CRT” optimization).
Where exactly is Links 2
It’s an amazing piece of software that is still getting compiled for OpenVMS, DOS, and OS/2 with EMX. It has a special mode for Braille readers where all menus are displayed full-screen. It is considered extremely stable, and very safe to use on the hostile web.
Links 2 upstream comes from https://links.twibright.com/. There doesn’t seem to be a Git/SVN/CVS repo accessible anywhere. You just download a source tarball.
But then there is one place where I found more, and it was a once-mirror of no-longer-online self-hosted “Polish Linux Distribution”s links2 package spec github.com/pld-linux/links2 containing a bunch of patches applied by the rpm spec therein, currently building the 2.30 version with the following patches:
This section contains, in spec order, a bunch patches patches by PLD Linux contributors
Note: copyright (earlier than 2012-2023) belongs to rkd77, megabajt, qboosh, grzegol, et al (PLD Linux contributors). May be presumed GPLv2
Patch for “glinks” executable symlink/alias to start in graphical mode
--- links-2.2/main.c 2007-12-11 17:45:38.000000000 +0100 +++ links-2.2/main.c.new 2010-01-21 22:00:22.020403979 +0100 @@ -4,6 +4,9 @@ * This file is a part of the Links program, released under GPL. */ +#define _GNU_SOURCE +#include <string.h> + #include "links.h" int retval = RET_OK; @@ -290,6 +290,7 @@ static void fixup_g(void) { + if (strncmp(basename(path_to_exe), "glinks", 6) == 0) ggr = 1; if (ggr_drv[0] || ggr_mode[0] || force_g) ggr = 1; if (dmp) ggr = 0; }
Patch for to enable pkg-config in autoconf for things like library dependencies
--- links-2.28/configure.in.orig 2022-10-07 20:47:33.616006730 +0200 +++ links-2.28/configure.in 2022-10-07 22:59:34.119764346 +0200 @@ -536,6 +536,8 @@ fi AC_CHECK_LIB(bsd, strmode) +PKG_PROG_PKG_CONFIG + dnl User option AC_MSG_CHECKING([for requested debug level])
Patch for showing part of src attribute for images when there is no better fallback than [IMG]
--- links-2.7/html.c.orig 2013-06-23 18:17:51.248954518 +0200 +++ links-2.7/html.c 2013-06-23 18:30:42.988922132 +0200 @@ -1062,7 +1062,54 @@ add_to_strn(&al, cast_uchar "]"); } else if (usemap) al = stracpy(cast_uchar "[USEMAP]"); else if (ismap) al = stracpy(cast_uchar "[ISMAP]"); - else al = stracpy(cast_uchar "[IMG]"); + else{ + unsigned char *str = get_attr_val(a, "src"); + unsigned char *s; + int r, i; + /* How images will be displayed: + * fake_alt = 0 -- do not truncate long names, + * fake_alt = 30 -- truncate long names to 30% of term width, + * fake_alt = 100 -- truncate long names to 100% of term width. + */ + int fake_alt = 20; + int max_len; + int name_len; + /* substitute string for hidden characters */ + unsigned char *fake_str = stracpy("*"); + if(str && fake_alt){ + /* FIXME: replace following '80' with screen width */ + max_len = (int)80*((float)fake_alt/100); + r = strcspn(str, "?"); + if (!(s = mem_alloc((r + 1) * sizeof(char)))) return; + strncpy(s, str, r); + s[r] = '\0'; + for(r = strlen(s) - 1; r >= 0; --r) + if(dir_sep(s[r])) break; + r++; + if(strlen(s + r) > max_len){ + for(i = strlen(s) -1; i>=0; --i) + if(s[i] == '.') break; + if(max_len < strlen(s + i)) al = stracpy("[IMG]"); + else{ + if(!(al = mem_alloc((max_len + strlen(fake_str) + 3) * sizeof(char)))) return; + name_len = max_len - strlen(s + i); + strcpy(al, "[\0"); + strncat(al, s + r, name_len/2); + strcat(al, fake_str); + strcat(al, s + r + (strlen(s + r) - max_len + name_len/2)); + strcat(al, "]"); + } + } + else{ + if(!(al = mem_alloc((strlen(s + r) + 3) * sizeof(char)))) return; + sprintf(al, "[%s]", s + r); + } + mem_free(s); + mem_free(str); + mem_free(fake_str); + } + else al = stracpy("[IMG]"); + } } if (al) { if (ismap) {
Patch for providing a migration path for users with bookmark file from old versions
--- links-2.22/bookmark.c.old 2021-03-20 18:44:34.524720442 +0100 +++ links-2.22/bookmark.c 2021-03-20 18:45:06.331385178 +0100 @@ -785,6 +785,18 @@ msg_box(ses->term, getml(f, NULL), TEXT_(T_BOOKMARK_ERROR), AL_CENTER, TEXT_(T_UNABLE_TO_WRITE_TO_BOOKMARK_FILE), cast_uchar " ", f, cast_uchar ": ", get_err_msg(err, ses->term), MSG_BOX_END, NULL, 1, TEXT_(T_CANCEL), msg_box_null, B_ENTER | B_ESC); } } + /* try to create bookmarks.html based on old bookmarks (from links <= 0.97) */ + if (access(bookmarks_file, R_OK) != 0) { + char *prev; + + if ((prev = get_current_dir_name()) && chdir(links_home) == 0) { + if (access("bookmarks", R_OK) == 0 && access("/usr/bin/perl", X_OK) == 0) { + system("/usr/bin/perl -lne '@l = split(q(\\|)); print qq($l[0])' bookmarks > bookmarks.html"); + } + chdir(prev); + free(prev); + } + } EINTRLOOP(rs, stat(cast_const_char bookmarks_file, &bookmarks_st)); if (rs)
Patch for using ~/.links2 rather than ~/.links, and analogously
--- links-2.1pre16/links.1.orig 2005-01-22 21:51:55.000000000 +0100 +++ links-2.1pre16/links.1 2005-01-28 20:47:50.217234704 +0100 @@ -303,7 +303,7 @@ .SH FILES .TP -.IP "~/.links/links.cfg" +.IP "~/.links2/links.cfg" Per-user configfile, automatically created by .B links. .SH PLATFORMS --- links-2.22/default.c.old 2021-03-20 19:10:17.237960076 +0100 +++ links-2.22/default.c 2021-03-20 19:10:57.477957622 +0100 @@ -797,7 +797,7 @@ while (home_links[0] && dir_sep(home_links[strlen(cast_const_char home_links) - 1])) home_links[strlen(cast_const_char home_links) - 1] = 0; EINTRLOOP(rs, stat(cast_const_char home_links, &st)); if (!rs && S_ISDIR(st.st_mode)) { - add_to_strn(&home_links, cast_uchar "/links"); + add_to_strn(&home_links, cast_uchar "/links2"); } else { fprintf(stderr, "CONFIG_DIR set to %s. But directory %s doesn't exist.\n\007", config_dir, home_links); portable_sleep(3000); @@ -810,9 +810,9 @@ #if defined(DOS) add_to_strn(&home_links, cast_uchar "links.cfg"); #elif defined(OPENVMS) || defined(HAIKU) - add_to_strn(&home_links, cast_uchar "links"); + add_to_strn(&home_links, cast_uchar "links2"); #else - add_to_strn(&home_links, cast_uchar ".links"); + add_to_strn(&home_links, cast_uchar ".links2"); #endif } EINTRLOOP(rs, stat(cast_const_char home_links, &st)); @@ -840,7 +840,7 @@ #ifdef DOS add_to_strn(&home_links, cast_uchar "links.cfg"); #else - add_to_strn(&home_links, cast_uchar "links"); + add_to_strn(&home_links, cast_uchar "links2"); #endif EINTRLOOP(rs, stat(cast_const_char home_links, &st)); if (rs) {
Patch for opening non-existent filepaths where a .gz-suffixed gzipped filed exists
--- links-2.10/file.c.gzip 2015-07-15 12:17:21.831370450 +0200 +++ links-2.10/file.c 2015-07-15 12:17:25.037715949 +0200 @@ -224,11 +224,17 @@ void file_func(struct connection *c) abort_connection(c); return; } +opening: if (!(name = get_filename(c->url))) { setcstate(c, S_OUT_OF_MEM); abort_connection(c); return; } EINTRLOOP(rs, stat(cast_const_char name, &stt)); if (rs) { + if (strncmp(c->url + strlen(c->url) - 3, ".gz", 3) != 0) { + add_to_strn(&c->url, ".gz"); + mem_free(name); + goto opening; + } mem_free(name); setcstate(c, get_error_from_errno(errno)); abort_connection(c); return; }
Patch for Polish translations
Introducing strings for menu items like saving clipboard to file, DNS preferences, cookies, fonts, windows, and original authors’ URLs.
--- links-2.29/intl/polish.lng~ 2023-03-09 19:13:27.000000000 +0100 +++ links-2.29/intl/polish.lng 2023-03-23 09:55:27.103304736 +0100 @@ -656,8 +656,8 @@ T_HK_DOCUMENT_INFO, "I", T_HK_HEADER_INFO, "F", T_HK_FRAME_AT_FULL_SCREEN, "Y", -T_HK_SAVE_CLIPBOARD_TO_A_FILE, NULL, -T_HK_LOAD_CLIPBOARD_FROM_A_FILE, NULL, +T_HK_SAVE_CLIPBOARD_TO_A_FILE, "Zapis schowka do pliku", +T_HK_LOAD_CLIPBOARD_FROM_A_FILE, "Odczyt schowka z pliku", T_HK_HTML_OPTIONS, "U", T_HK_COLOR, "K", T_HK_SAVE_HTML_OPTIONS, "Z", @@ -671,14 +671,14 @@ T_HK_IPV6_OPTIONS, "I", T_HK_PROXIES, "P", T_HK_SSL_OPTIONS, "L", -T_HK_DNS_OPTIONS, NULL, +T_HK_DNS_OPTIONS, "Opcje DNS", T_HK_HTTP_OPTIONS, "H", T_HK_FTP_OPTIONS, "F", T_HK_SMB_OPTIONS, "S", T_HK_JAVASCRIPT_OPTIONS, "J", T_HK_MISCELANEOUS_OPTIONS, "N", -T_HK_COOKIES, NULL, -T_HK_FONTS, NULL, +T_HK_COOKIES, "Ciasteczka", +T_HK_FONTS, "Fonty", T_HK_CACHE, "P", T_HK_MAIL_AND_TELNEL, "C", T_HK_ASSOCIATIONS, "S", @@ -695,7 +695,7 @@ T_HK_VIEW, "W", T_HK_LINK, "L", T_HK_DOWNLOADS, "O", -T_HK_WINDOWS, NULL, +T_HK_WINDOWS, "Okna", T_HK_SETUP, "U", T_HK_HELP, "M", T_HK_DISPLAY_USEMAP, "M", @@ -717,6 +717,6 @@ T_HK_WINDOW, "O", T_HK_FULL_SCREEN, "P", T_HK_BEOS_TERMINAL, "B", -T_URL_MANUAL, NULL, -T_URL_HOMEPAGE, NULL, -T_URL_CALIBRATION, NULL, +T_URL_MANUAL, "http://links.twibright.com/user_en.html", +T_URL_HOMEPAGE, "http://links.twibright.com/", +T_URL_CALIBRATION, "http://links.twibright.com/calibration.html",
Patch replacing an Automake macro with an Autoconf one
--- links-2.22/configure.in.old 2021-03-20 19:16:29.597937288 +0100 +++ links-2.22/configure.in 2021-03-20 19:16:50.644602655 +0100 @@ -18,7 +18,7 @@ export LDFLAGS export LIBS -AM_CONFIG_HEADER(config.h) +AC_CONFIG_HEADERS(config.h) dnl Checks for programs. AC_PROG_CC
Patch enabling Autoconf detection of a C++ compiler
--- links-2.7/configure.in.old 2013-04-06 20:56:02.641704629 +0200 +++ links-2.7/configure.in 2013-04-06 20:56:18.208612038 +0200 @@ -18,7 +18,7 @@ AC_CONFIG_HEADERS(config.h) dnl Checks for programs. AC_PROG_CC -dnl AC_PROG_CXX +AC_PROG_CXX dnl AC_PROG_AWK dnl AM_PROG_LEX dnl AC_PROG_YACC
So, I just pasted a bunch of patches from a legacy Linux distribution into my blog post.
This is a first for me in the patch-oriented development practices. I had to learn how to apply those — for that, I learned I need to use the quilt tool for patch management, that let’s me either create a series file, or should be able to just take in the spec file that is present in the repo. I, perhaps needlessly, did the former, by listing the files in order into a file named series and putting it in my clone of github.com/pld-linux/links2 cloned as patches directory name in the directory of extracted links-2.30 tarball. And ran «quilt push -a» to apply them.
My goals
I think I’m gonna like Links as a base — by implementing a bunch of patches of my own — for a browser that can do
Gemini protocol
adding links to some special bookmark folders rather than opening them, perhaps a tour-like UX but less linear
interoperate with a lightweight feed reader that opens links in a browser, or display an Atom/RSS feed on its own
Gopher protocol
Basically, yeah, Offpunk browser is cool but I want it more calming to the eye, somewhat featureful for smolweb content presentation, and usable with a mouse.
Since this is another project where I will be doing experimental changes for own ergonomics, some of my patches might end up being exotic — think, mouse chording for some reason.
Takahe has limited support for this type: See Original ArticleJust seeing what time was the last update to an RSS feed should be enough for most. And the RSS feeds already have titles, so one might not need a full OPML, but nothing more than a list of URLs as a flat file.
Although what follows has some room for improvement, feed reading can look like this
Feeds with text content layed out into a frameset by this script will be comfortable to read in graphical mode of Links browser. Framesets are great for sidebar UI. In the bottom part of the sidebar you have the selection of feeds. The script can embed you the original date string in the article view, and a time-units-since suffix into the feed selection document. It can be seen that a Unicode emoji is displayed by Links as a “blast” character at least in this compilation.Some blogs will only provide a snippet into the feed, and thus the script renders that as a “Read full article” link, that leads directly to the article, right in the browser. You can navigate back from it when you’re done reading. It can be seen that a Unicode apostrophe in a link is rendered by Links as three asterisks at least in this compilation.Framesets are also still supported by modern browsers, so this is a neat option for usual users as well. You fetch the feeds once and then you are in just a bunch of local static HTML files that happen to be a frameset.My idea consists of two programs, for potential composability and to not bother with any kind of PATH:
one gets spawned in a directory and lays out the articles from the feed XML passed to it on standard input,
and one gets passed a list of feed URLs on standard input, and an argument with the path to the former script (it is not hardcoded to be composed with just that first one, although expects from the provided one not just the nav.html that it will link but also a title.txt and a date.txt in the directory after its execution) — and it creates the index.html with the frameset (constant content) and a feeds.html with the list of feeds and when were they last updated
#!/usr/bin/env perl use v5.36; use strict; use warnings; use XML::LibXML; use File::Path qw(make_path); my $dom = XML::LibXML->load_xml(IO => \*STDIN); my $xc = XML::LibXML::XPathContext->new($dom); $xc->registerNs(atom => 'http://www.w3.org/2005/Atom'); sub safe_id ($id, $fallback) { my $base = ($id && $id =~ /\S/) ? $id : $fallback; $base =~ s/[^A-Za-z0-9._-]/_/g; return $base; } sub text_of ($node, $xpath) { my ($n) = $xc->findnodes($xpath, $node); return defined $n ? $n->textContent : undef; } sub write_file ($path, $content) { open my $fh, '>:encoding(UTF-8)', $path or die "Cannot write $path: $!"; print $fh $content; close $fh; } # --- my @entries; if ($xc->exists('/rss/channel/item')) { @entries = $xc->findnodes('/rss/channel/item'); } else { @entries = $xc->findnodes('/atom:feed/atom:entry'); } my $feed_title = text_of($dom, '/rss/channel/title') // text_of($dom, '/atom:feed/atom:title') // 'Feed'; my $feed_date = text_of($dom, '/rss/channel/lastBuildDate') // text_of($dom, '/atom:feed/atom:updated'); write_file('title.txt', $feed_title); write_file('date.txt', $feed_date); # --- my $nav = "<html><head><title>$feed_title</title></head><body>"; $nav .= "<h2>$feed_title</h2><ul>"; for my $i (0 .. $#entries) { my $e = $entries[$i]; my $raw_id = text_of($e, './guid') // text_of($e, './atom:id'); my $id = safe_id($raw_id, "entry-" . ($i + 1)); my $title = text_of($e, './title') // text_of($e, './atom:title') // 'Untitled'; $nav .= qq{<li><a target="main" href="$id.html">$title</a></li>}; } $nav .= "</ul></body></html>"; write_file('nav.html', $nav); # --- for my $i (0 .. $#entries) { my $e = $entries[$i]; my $raw_id = text_of($e, './guid') // text_of($e, './atom:id'); my $id = safe_id($raw_id, "entry-" . ($i + 1)); my $title = text_of($e, './title') // text_of($e, './atom:title') // 'Untitled'; my $date = text_of($e, './pubDate') // text_of($e, './atom:updated') // text_of($e, './atom:published') // ''; my $body = text_of($e, './description') // text_of($e, './atom:summary') // text_of($e, './atom:content') // ''; my $url = text_of($e, './link') // text_of($e, './atom:link[@rel="alternate"]/@href') // text_of($e, './atom:link[1]/@href'); my $html = <<"HTML"; <html> <head><title>$title</title></head> <body> <h1>$title</h1> <p>$date</p> <p>$body</p> HTML if ($url) { $html .= qq{<p><a target="_top" href="$url">Read full article</a></p>}; } $html .= "</body></html>"; write_file("$id.html", $html); } say "Done.";
#!/usr/bin/env perl use v5.36; use strict; use warnings; use String::Util 'trim'; use File::Path qw(make_path); use IPC::Run qw(start); sub write_file ($path, $content) { open my $fh, '>:encoding(UTF-8)', $path or die "Cannot write $path: $!"; print $fh $content; close $fh; } write_file('index.html', <<"HTML"); <html> <frameset cols="25%,75%"> <frameset rows="75%,25%"> <frame name="nav"/> <frame src="feeds.html" name="feeds"/> </frameset> <frame name="main"/> </frameset> </html> HTML use Cwd 'abs_path'; my $workprog = abs_path $ARGV[0]; my @jobs; my @urls; while (<STDIN>) { chomp; my $dir = $_; $dir =~ s/[^A-Za-z0-9._-]/_/g; make_path($dir); push @urls, $_; push @jobs, start( [ qw/curl -sL/, $_ ], "|", [ $workprog ], init => sub { chdir $dir }); } use Time::Piece; use Time::Seconds; sub parse_date ($s) { for ( "%Y-%m-%dT%H:%M:%S%z", "%a, %d %b %Y %H:%M:%S %z", "%Y-%m-%dT%H:%M%z", ) { $s =~ s/([+-]\d\d):(\d\d)/$1$2/; my $t = eval { Time::Piece->strptime($s, $_) }; return $t if $t; } return; } sub human ($t) { my $d = gmtime() - $t; $d < ONE_HOUR ? int($d->minutes)."m ago" : $d < ONE_DAY ? int($d->hours)."h ago" : int($d->days)."d ago"; } open my $fh, '>:encoding(UTF-8)', 'feeds.html' or die "cannot write feeds.html: $!"; use Path::Tiny; for (@jobs) { $_->finish; $_ = shift @urls; s/[^A-Za-z0-9._-]/_/g; my $title = path("$_/title.txt")->slurp_utf8; $title =~ s/[\p{Cf}\p{Mn}\p{So}\p{Sk}]//g; $title = substr($title, 0, 20); $title = trim $title; my $date = path("$_/date.txt")->slurp_utf8; $date = parse_date $date; $date = human $date; print {$fh} <<"HTML"; <p><a href="$_/nav.html" target=nav>$title ++ $date</a> HTML } close $fh;
You execute them by running the second one with the path to the first one as an argument (~/feeds_many.pl ~/feeds_one.pl < ~/feeds), and redirect a list of bare URLs as lines to it — it creates you the directory structure and the HTML files; you then navigate with your browser to the index.html, which you of course can do all in a single shell command line.
Run once, read some, run once the next day again.
So, I hope to use it from now on 🙂 and read feeds at last.
In my simplistic ideas, I nonetheless may have been influenced by the approach of Offpunk browser. https://offpunk.net
Takahe has limited support for this type: See Original Article