gem.awk

A gemini client in POSIX awk
git clone git://git.vgx.fr/gem.awk
Log | Files | Refs

commit dc346b9766696fa6a66f0306b036af79d70048ca
Author: Léo Villeveygoux <l@vgx.fr>
Date:   Tue,  2 Jun 2020 01:49:42 +0200

First version of gem.awk gemini client

Uses socat for TLS connexion at the moment.
No scrolling control implemented, you'll have to rely on your term's
scrollback buffer.

Implemented:

- opening gemini:// links,
- text/gemini :
  - display with minimal formatting
  - list headers
  - list and number links
  - follow links
- display text/* files
- handle other MIME types with redirections (not tested...)
- display errors

Maybe TODO:
- redirect handling
- scrolling/better wrapping
- history
- try to rlwrap it
- link redirections
- maybe switch to "openssl s_client"? (I have some TLS errors with socat)

Diffstat:
Agem.awk | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 156 insertions(+), 0 deletions(-)

diff --git a/gem.awk b/gem.awk @@ -0,0 +1,156 @@ +#!/usr/bin/awk -f + +function parse_gemini(socat_cmd) { + PAGE_URL=CURRENT_URL + PAGE_LINE_NUM=0 + PAGE_LINK_NUM=0 + PAGE_TITLE_NUM=0 + pre=0 + for (i in PAGE_LINES) delete PAGE_LINES[i] + for (i in PAGE_LINKS) delete PAGE_LINKS[i] + for (i in PAGE_TITLES) delete PAGE_TITLES[i] + + while (socat_cmd | getline) { + if (!pre) { + if (/^=>/) { + PAGE_LINKS[++PAGE_LINK_NUM]=length($1) == 2 ? $2 : substr($1, 2) + match($0, /^=>[ \t]*[^ \t]+[ \t]*/) + $0="=> [" PAGE_LINK_NUM "] \033[4m" PAGE_LINKS[PAGE_LINK_NUM] "\033[0m " substr($0, RSTART + RLENGTH) + } else if (/^#/) { + PAGE_TITLES[++PAGE_TITLE_NUM]=PAGE_LINE_NUM + 1 + $0="\033[1;4m" $0 "\033[0m" + } + } + + if (/^```/) + pre = pre ? 0 : 1 + else + PAGE_LINES[++PAGE_LINE_NUM]=$0 + } + + for (i in PAGE_LINES) + print PAGE_LINES[i] +} + +function print_text(socat_cmd) { + while (socat_cmd | getline) + print +} + +function socat_open(domain, path, port) { + if (!port) + port = 1965 + socat_cmd="echo 'gemini://" domain "/" path "\r' | socat - 'SSL:" domain ":" port "'" + socat_cmd | getline +} + +function plumb_out(socat_cmd, out) { + system(socat_cmd " | tail +2 " out ) +} + +function gemini_url_open(url) { + split(url, path_elt, "/") + domain=path_elt[3] + port=1965 + if (match(domain, /:[[:digit:]]+$/)) { + port=substr(domain, RSTART + 1) + domain=substr(domain, 1, RSTART - 1) + } + path=url + sub(/gemini:\/\/[^\/]+\/?/, "", path) + + socat_open(domain, path, port) + + if (/^2./) { + CURRENT_URL=url + if (!$2 || $2 ~ /text\/gemini/) + parse_gemini(socat_cmd) + else if ($2 ~ /^text/) + print_text(socat_cmd) + else { + close(socat_cmd) + print "Binary filetype: " $2 + print "Blank to ignore, '| cmd' or '> file' to redirect" + prompt() + getline + if (/^[|>]/) + plumb_out(socat_cmd, $0) + else + print "Ignored." + } + } else { + close(socat_cmd) + print "Error: " $0 + } +} + +function prompt() { + printf "> " +} + +function help() { + print "The following commands are available:" + print " gemini://\033[3;4m.*\033[0m : open a gemini URL" + print " \033[3;4mn\033[0m : follow link \033[3;4mn\033[0m of current text/gemini page" + print " toc : list titles in a text/gemini page" + print " links : list URLs linked in a text/gemini page" + print " help : show this help" +} + +BEGIN { + help() + prompt() +} + +/^gemini:\/\// { + gemini_url_open($1) +} + +$1 ~ /^[[:digit:]]+$/ { + if ($1 == 0 || $1 > PAGE_LINK_NUM) { + print "No link with id " $1 + next + } + + url=PAGE_LINKS[$1] + if (url ~ /^[^:]+(\/.*)?$/) { + # relative link + gemini_url_open(PAGE_URL "/" url) + } else if (url ~ /^gemini:\/\//) { + gemini_url_open(url) + } else { + print "Not a gemini URL, open with (blank to ignore):" + prompt() + getline + if($0) + system($0 " '" url "'") + } +} + +$1 == "toc" { + if (!PAGE_TITLE_NUM) { + print "No title found." + next + } + for(i in PAGE_TITLES) + print PAGE_LINES[PAGE_TITLES[i]] ": " PAGE_TITLES[i] +} + +$1 == "links" { + if (!PAGE_LINK_NUM) { + print "No link found." + next + } + for(i in PAGE_LINKS) + print "=> [" i "] \033[4m" PAGE_LINKS[i] "\033[0m" +} + +$1 == "help" { + help() +} + +{ prompt() } + +END { + print "Bye!" +}