bollux/bollux

1081 lines
23 KiB
Plaintext
Raw Normal View History

2020-05-22 20:53:35 +00:00
#!/usr/bin/env bash
2020-05-26 02:41:42 +00:00
# bollux: a bash gemini client
# Author: Case Duckworth
# License: MIT
2020-06-05 01:14:05 +00:00
# Version: 0.4.0
2020-05-26 02:37:55 +00:00
# Program information
PRGN="${0##*/}"
2020-06-05 01:14:05 +00:00
VRSN=0.4.0
2020-05-26 02:37:55 +00:00
2020-05-26 02:41:42 +00:00
bollux_usage() {
cat <<END
$PRGN (v. $VRSN): a bash gemini client
usage:
$PRGN [-h]
$PRGN [-q] [-v] [URL]
flags:
-h show this help and exit
-q be quiet: log no messages
-v verbose: log more messages
parameters:
URL the URL to start in
If not provided, the user will be prompted.
END
}
2020-06-08 00:23:14 +00:00
run() { # run COMMAND...
2021-02-25 23:01:10 +00:00
trap bollux_quit SIGINT
2020-06-08 00:23:14 +00:00
log debug "$*"
2020-05-26 02:37:55 +00:00
"$@"
}
2020-06-08 00:23:14 +00:00
die() { # die EXIT_CODE MESSAGE
2020-06-05 01:14:05 +00:00
local ec="$1"
2020-05-26 02:37:55 +00:00
shift
log error "$*"
exit "$ec"
2020-05-24 00:45:21 +00:00
}
2020-05-22 20:53:35 +00:00
2020-06-08 20:37:03 +00:00
# builtin replacement for `sleep`
# https://github.com/dylanaraps/pure-bash-bible#use-read-as-an-alternative-to-the-sleep-command
sleep() { # sleep SECONDS
read -rt "$1" <> <(:) || :
}
2020-06-08 00:23:14 +00:00
# https://github.com/dylanaraps/pure-bash-bible/
trim_string() { # trim_string STRING
2020-05-30 19:35:17 +00:00
: "${1#"${1%%[![:space:]]*}"}"
: "${_%"${_##*[![:space:]]}"}"
printf '%s\n' "$_"
}
2020-05-26 02:37:55 +00:00
2020-06-08 00:23:14 +00:00
log() { # log LEVEL MESSAGE
2020-05-26 02:37:55 +00:00
[[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
2020-06-05 01:14:05 +00:00
local fmt
2020-05-22 20:53:35 +00:00
case "$1" in
2020-06-04 04:22:55 +00:00
[dD]*) # debug
2020-05-26 02:37:55 +00:00
[[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return
fmt=34
2020-05-22 22:12:15 +00:00
;;
2020-06-04 04:22:55 +00:00
[eE]*) # error
2020-05-26 02:37:55 +00:00
fmt=31
2020-05-22 20:53:35 +00:00
;;
2020-05-26 02:37:55 +00:00
*) fmt=1 ;;
2020-05-22 20:53:35 +00:00
esac
2020-05-26 02:37:55 +00:00
shift
2020-06-05 01:14:05 +00:00
2020-06-08 15:13:34 +00:00
printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*"
2020-05-26 02:37:55 +00:00
}
2020-05-22 13:38:40 +00:00
2020-05-26 02:37:55 +00:00
# main entry point
bollux() {
2020-06-05 01:14:05 +00:00
run bollux_config # TODO: figure out better config method
run bollux_args "$@" # and argument parsing
run bollux_init
2020-05-26 02:37:55 +00:00
2020-06-04 04:22:55 +00:00
if [[ ! "${BOLLUX_URL:+x}" ]]; then
2020-05-26 02:37:55 +00:00
run prompt GO BOLLUX_URL
2020-05-22 20:53:35 +00:00
fi
2020-05-26 02:37:55 +00:00
2020-06-08 00:23:14 +00:00
log d "BOLLUX_URL='$BOLLUX_URL'"
2020-06-18 13:24:01 +00:00
run blastoff -u "$BOLLUX_URL"
2020-05-22 13:38:40 +00:00
}
2020-06-08 00:23:14 +00:00
# process command-line arguments
2020-05-26 02:37:55 +00:00
bollux_args() {
2020-05-26 02:41:42 +00:00
while getopts :hvq OPT; do
2020-05-26 02:37:55 +00:00
case "$OPT" in
2020-05-26 02:41:42 +00:00
h)
bollux_usage
exit
;;
2020-05-26 02:37:55 +00:00
v) BOLLUX_LOGLEVEL=DEBUG ;;
q) BOLLUX_LOGLEVEL=QUIET ;;
:) die 1 "Option -$OPTARG requires an argument" ;;
*) die 1 "Unknown option: -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
if (($# == 1)); then
BOLLUX_URL="$1"
fi
}
2020-05-22 20:53:35 +00:00
2020-06-08 00:23:14 +00:00
# process config file and set variables
2020-05-26 02:37:55 +00:00
bollux_config() {
2020-05-31 13:40:26 +00:00
: "${BOLLUX_CONFIG:=${XDG_CONFIG_DIR:-$HOME/.config}/bollux/bollux.conf}"
2020-05-26 02:37:55 +00:00
if [ -f "$BOLLUX_CONFIG" ]; then
# shellcheck disable=1090
. "$BOLLUX_CONFIG"
else
log debug "Can't load config file '$BOLLUX_CONFIG'."
fi
2020-05-30 21:00:12 +00:00
## behavior
2020-06-08 20:24:43 +00:00
: "${BOLLUX_TIMEOUT:=30}" # connection timeout
: "${BOLLUX_MAXREDIR:=5}" # max redirects
: "${BOLLUX_PORT:=1965}" # port number
: "${BOLLUX_PROTO:=gemini}" # default protocol
: "${BOLLUX_URL:=}" # start url
: "${BOLLUX_BYEMSG:=See You Space Cowboy ...}" # bye message
2020-06-03 03:24:49 +00:00
## files
: "${BOLLUX_DATADIR:=${XDG_DATA_DIR:-$HOME/.local/share}/bollux}"
: "${BOLLUX_DOWNDIR:=.}" # where to save downloads
: "${BOLLUX_LESSKEY:=$BOLLUX_DATADIR/lesskey}" # where to store binds
: "${BOLLUX_PAGESRC:=$BOLLUX_DATADIR/pagesrc}" # where to save the source
BOLLUX_HISTFILE="$BOLLUX_DATADIR/history" # where to save the history
2020-05-30 21:00:12 +00:00
## typesetting
2021-02-26 01:36:14 +00:00
: "${T_MARGIN:=4}" # left and right margin
: "${T_WIDTH:=0}" # width of the viewport -- 0 = get term width
2020-05-30 21:00:12 +00:00
# colors -- these will be wrapped in \e[ __ m
C_RESET='\e[0m' # reset
: "${C_SIGIL:=35}" # sigil (=>, #, ##, ###, *, ```)
: "${C_LINK_NUMBER:=1}" # link number
: "${C_LINK_TITLE:=4}" # link title
: "${C_LINK_URL:=36}" # link URL
: "${C_HEADER1:=1;4}" # header 1 formatting
: "${C_HEADER2:=1}" # header 2 formatting
: "${C_HEADER3:=3}" # header 3 formatting
: "${C_LIST:=0}" # list formatting
: "${C_QUOTE:=3}" # quote formatting
2020-05-30 21:00:12 +00:00
: "${C_PRE:=0}" # preformatted text formatting
2020-06-18 13:24:01 +00:00
## state
UC_BLANK=':?:'
2020-05-22 13:38:40 +00:00
}
2020-06-08 00:23:14 +00:00
# quit happily
2020-05-31 15:58:44 +00:00
bollux_quit() {
2020-06-08 20:24:43 +00:00
printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
2020-05-31 15:58:44 +00:00
exit
}
2021-02-25 23:01:10 +00:00
# trap C-c
trap bollux_quit SIGINT
2020-05-31 15:58:44 +00:00
2020-06-08 00:23:14 +00:00
# set the terminal title
set_title() { # set_title STRING
2020-06-07 23:43:28 +00:00
printf '\e]2;%s\007' "$*"
2020-05-31 15:58:44 +00:00
}
2020-06-08 00:23:14 +00:00
# prompt for input
2020-06-03 12:43:46 +00:00
prompt() { # prompt [-u] PROMPT [READ_ARGS...]
2020-06-05 01:14:05 +00:00
local read_cmd=(read -e -r)
2020-06-03 12:43:46 +00:00
if [[ "$1" == "-u" ]]; then
read_cmd+=(-i "$BOLLUX_URL")
shift
fi
2020-06-05 01:14:05 +00:00
local prompt="$1"
shift
2020-06-03 12:43:46 +00:00
read_cmd+=(-p "$prompt> ")
"${read_cmd[@]}" </dev/tty "$@"
}
2020-06-08 00:23:14 +00:00
# load a URL
blastoff() { # blastoff [-u] URL
2020-06-18 13:24:01 +00:00
local u
2020-05-26 02:37:55 +00:00
if [[ "$1" == "-u" ]]; then
2020-06-18 13:24:01 +00:00
u="$(run uwellform "$2")"
else
u="$1"
2020-05-26 02:37:55 +00:00
fi
2020-06-18 13:24:01 +00:00
local -a url
run utransform url "$BOLLUX_URL" "$u"
if ! ucdef url[1]; then
run ucset url[1] "$BOLLUX_PROTO"
2020-05-26 02:37:55 +00:00
fi
2020-06-05 01:14:05 +00:00
{
2020-06-18 13:24:01 +00:00
if declare -Fp "${url[1]}_request" >/dev/null 2>&1; then
run "${url[1]}_request" "$url"
2020-06-05 01:14:05 +00:00
else
2020-06-18 13:24:01 +00:00
die 99 "No request handler for '${url[1]}'"
2020-06-05 01:14:05 +00:00
fi
2020-06-18 13:24:01 +00:00
} | run normalize | {
if declare -Fp "${url[1]}_response" >/dev/null 2>&1; then
run "${url[1]}_response" "$url"
else
log d "No response handler for '${url[1]}', passing thru"
passthru
fi
}
2020-05-26 02:37:55 +00:00
}
2020-06-18 13:24:01 +00:00
# URLS
## https://tools.ietf.org/html/rfc3986
uwellform() {
local u="$1"
2021-02-26 01:36:14 +00:00
2020-06-18 13:24:01 +00:00
if [[ "$u" != *://* ]]; then
u="$BOLLUX_PROTO://$u"
fi
u="$(trim_string "$u")"
printf '%s\n' "$u"
}
usplit() { # usplit NAME:ARRAY URL:STRING
local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
[[ $2 =~ $re ]] || return $?
# shellcheck disable=2034
local scheme="${BASH_REMATCH[2]}" \
authority="${BASH_REMATCH[4]}" \
path="${BASH_REMATCH[5]}" \
query="${BASH_REMATCH[7]}" \
fragment="${BASH_REMATCH[9]}"
2020-06-18 13:24:01 +00:00
# 0=url 1=scheme 2=authority 3=path 4=query 5=fragment
local i=1 c
for c in scheme authority path query fragment; do
if [[ "${!c}" || "$c" == path ]]; then
printf -v "$1[$i]" '%s' "${!c}"
else
# shellcheck disable=2059
2020-06-18 13:24:01 +00:00
printf -v "$1[$i]" "$UC_BLANK"
fi
2021-02-26 01:36:14 +00:00
((i += 1))
2020-06-18 13:24:01 +00:00
done
# shellcheck disable=2059
2020-06-18 13:24:01 +00:00
printf -v "$1[0]" "$(ujoin "$1")" # inefficient I'm sure
}
ujoin() { # ujoin NAME:ARRAY
local -n U="$1"
if ucdef U[1]; then
printf -v U[0] "%s:" "${U[1]}"
fi
if ucdef U[2]; then
printf -v U[0] "${U[0]}//%s" "${U[2]}"
fi
printf -v U[0] "${U[0]}%s" "${U[3]}"
2021-02-26 01:36:14 +00:00
2020-06-18 13:24:01 +00:00
if ucdef U[4]; then
printf -v U[0] "${U[0]}?%s" "${U[4]}"
fi
if ucdef U[5]; then
printf -v U[0] "${U[0]}#%s" "${U[5]}"
fi
2020-06-18 13:24:01 +00:00
log d "${U[0]}"
}
ucdef() { [[ "${!1}" != "$UC_BLANK" ]]; } # ucdef NAME
ucblank() { [[ -z "${!1}" ]]; } # ucblank NAME
2021-02-26 01:36:14 +00:00
ucset() { # ucset NAME VALUE
2020-06-18 13:24:01 +00:00
run eval "${1}='$2'"
2021-02-26 01:36:14 +00:00
run ujoin "${1/\[*\]/}"
2020-06-18 13:24:01 +00:00
}
2021-02-26 01:36:14 +00:00
utransform() { # utransform TARGET:ARRAY BASE:STRING REFERENCE:STRING
local -a B R # base, reference
2020-06-18 13:24:01 +00:00
local -n T="$1" # target
usplit B "$2"
usplit R "$3"
# initialize T
2021-02-26 01:36:14 +00:00
for ((i = 1; i <= 5; i++)); do
2020-06-18 13:24:01 +00:00
T[$i]="$UC_BLANK"
done
# 0=url 1=scheme 2=authority 3=path 4=query 5=fragment
if ucdef R[1]; then
T[1]="${R[1]}"
if ucdef R[2]; then
T[2]="${R[2]}"
fi
if ucdef R[3]; then
T[3]="$(pundot "${R[3]}")"
fi
if ucdef R[4]; then
T[4]="${R[4]}"
fi
else
2020-06-18 13:24:01 +00:00
if ucdef R[2]; then
T[2]="${R[2]}"
if ucdef R[2]; then
T[3]="$(pundot "${R[3]}")"
fi
if ucdef R[4]; then
T[4]="${R[4]}"
fi
2020-05-26 02:37:55 +00:00
else
2020-06-18 13:24:01 +00:00
if ucblank R[3]; then
T[3]="${B[3]}"
if ucdef R[4]; then
T[4]="${R[4]}"
else
2020-06-18 13:24:01 +00:00
T[4]="${B[4]}"
fi
else
2020-06-18 13:24:01 +00:00
if [[ "${R[3]}" == /* ]]; then
T[3]="$(pundot "${R[3]}")"
else
2020-06-18 13:24:01 +00:00
T[3]="$(pmerge B R)"
T[3]="$(pundot "${T[3]}")"
fi
if ucdef R[4]; then
T[4]="${R[4]}"
fi
fi
2020-06-18 13:24:01 +00:00
T[2]="${B[2]}"
2020-05-26 02:37:55 +00:00
fi
2020-06-18 13:24:01 +00:00
T[1]="${B[1]}"
2020-05-26 02:37:55 +00:00
fi
2020-06-18 13:24:01 +00:00
if ucdef R[5]; then
T[5]="${R[5]}"
fi
2020-06-18 13:24:01 +00:00
ujoin T
2020-05-26 02:37:55 +00:00
}
2020-05-22 20:53:35 +00:00
2020-06-18 13:24:01 +00:00
pundot() { # pundot PATH:STRING
local input="$1"
2020-06-05 01:14:05 +00:00
local output
while [[ "$input" ]]; do
if [[ "$input" =~ ^\.\.?/ ]]; then
input="${input#${BASH_REMATCH[0]}}"
elif [[ "$input" =~ ^/\.(/|$) ]]; then
input="/${input#${BASH_REMATCH[0]}}"
elif [[ "$input" =~ ^/\.\.(/|$) ]]; then
input="/${input#${BASH_REMATCH[0]}}"
[[ "$output" =~ /?[^/]+$ ]]
output="${output%${BASH_REMATCH[0]}}"
elif [[ "$input" == . || "$input" == .. ]]; then
input=
else
2020-06-18 13:24:01 +00:00
[[ $input =~ ^(/?[^/]*)(/?.*)$ ]] || return 1
output="$output${BASH_REMATCH[1]}"
input="${BASH_REMATCH[2]}"
fi
done
printf '%s\n' "${output//\/\//\//}"
}
2020-06-18 13:24:01 +00:00
pmerge() {
local -n b="$1"
local -n r="$2"
2020-06-18 13:24:01 +00:00
if ucblank r[3]; then
printf '%s\n' "${b[3]//\/\//\//}"
return
fi
2020-06-18 13:24:01 +00:00
if ucdef b[2] && ucblank b[3]; then
printf '/%s\n' "${r[3]//\/\//\//}"
else
local bp=""
if [[ "${b[3]}" == */* ]]; then
bp="${b[3]%/*}"
fi
printf '%s/%s\n' "${bp%/}" "${r[3]#/}"
fi
}
2020-06-08 00:23:14 +00:00
# https://github.com/dylanaraps/pure-bash-bible/
2020-06-18 13:24:01 +00:00
uencode() { # uencode URL:STRING
2020-06-08 00:23:14 +00:00
local LC_ALL=C
for ((i = 0; i < ${#1}; i++)); do
: "${1:i:1}"
case "$_" in
[a-zA-Z0-9.~_-])
printf '%s' "$_"
;;
*)
printf '%%%02X' "'$_"
;;
esac
done
printf '\n'
}
# https://github.com/dylanaraps/pure-bash-bible/
2021-02-26 01:36:14 +00:00
udecode() { # udecode URL:STRING
2020-06-08 00:23:14 +00:00
: "${1//+/ }"
printf '%b\n' "${_//%/\\x}"
2020-06-05 01:14:05 +00:00
}
# GEMINI
2020-06-08 00:23:14 +00:00
# https://gemini.circumlunar.space/docs/specification.html
gemini_request() { # gemini_request URL
2020-06-18 13:24:01 +00:00
local -a url
usplit url "$1"
# get rid of userinfo
ucset url[2] "${url[2]#*@}"
local port
if [[ "${url[2]}" == *:* ]]; then
port="${url[2]#*:}"
ucset url[2] "${url[2]%:*}"
else
port=1965 # TODO variablize
fi
2020-06-18 13:24:01 +00:00
local ssl_cmd=(
2021-02-26 01:36:14 +00:00
openssl s_client
-crlf -quiet -connect "${url[2]}:$port"
-servername "${url[2]}" # SNI
-no_ssl3 -no_tls1 -no_tls1_1 # disable old TLS/SSL versions
2020-06-18 13:24:01 +00:00
)
2020-06-18 13:24:01 +00:00
run "${ssl_cmd[@]}" <<<"$url"
2020-05-22 13:38:40 +00:00
}
2020-06-08 00:23:14 +00:00
gemini_response() { # gemini_response URL
2020-06-05 01:14:05 +00:00
local url code meta
local title
url="$1"
# we need a loop here so it waits for the first line
2020-06-07 18:34:59 +00:00
while read -t "$BOLLUX_TIMEOUT" -r code meta ||
2020-06-05 01:14:05 +00:00
{ (($? > 128)) && die 99 "Timeout."; }; do
break
done
2020-05-22 13:38:40 +00:00
2020-06-04 04:22:55 +00:00
log d "[$code] $meta"
2020-05-22 20:53:35 +00:00
case "$code" in
2020-06-05 01:14:05 +00:00
1*) # input
2020-05-26 02:37:55 +00:00
REDIRECTS=0
BOLLUX_URL="$url"
2020-06-07 21:51:48 +00:00
case "$code" in
10) run prompt "$meta" ;;
11) run prompt "$meta" -s ;; # password input
esac
2020-06-18 13:24:01 +00:00
run blastoff "?$(uencode "$REPLY")"
2020-05-22 13:38:40 +00:00
;;
2020-06-05 01:14:05 +00:00
2*) # OK
2020-05-26 02:37:55 +00:00
REDIRECTS=0
BOLLUX_URL="$url"
# read ahead to find a title
2020-06-05 01:14:05 +00:00
local pretitle
while read -r; do
2020-06-03 03:37:27 +00:00
pretitle="$pretitle$REPLY"$'\n'
if [[ "$REPLY" =~ ^#[[:space:]]*(.*) ]]; then
title="${BASH_REMATCH[1]}"
break
fi
done
2020-06-05 01:14:05 +00:00
run history_append "$url" "${title:-}"
# read the body out and pipe it to display
{
printf '%s' "$pretitle"
2020-06-05 01:14:05 +00:00
passthru
} | run display "$meta" "${title:-}"
2020-05-22 13:38:40 +00:00
;;
2020-06-05 01:14:05 +00:00
3*) # redirect
2020-05-26 02:37:55 +00:00
((REDIRECTS += 1))
if ((REDIRECTS > BOLLUX_MAXREDIR)); then
die $((100 + code)) "Too many redirects!"
fi
2020-06-08 20:30:20 +00:00
BOLLUX_URL="$url"
2020-06-05 01:14:05 +00:00
run blastoff "$meta" # TODO: confirm redirect
2020-05-22 13:38:40 +00:00
;;
2020-06-05 01:14:05 +00:00
4*) # temporary error
2020-05-26 02:37:55 +00:00
REDIRECTS=0
2020-06-05 01:14:05 +00:00
die "$((100 + code))" "Temporary error [$code]: $meta"
2020-05-22 13:38:40 +00:00
;;
2020-06-05 01:14:05 +00:00
5*) # permanent error
2020-05-26 02:37:55 +00:00
REDIRECTS=0
2020-06-05 01:14:05 +00:00
die "$((100 + code))" "Permanent error [$code]: $meta"
2020-05-22 13:38:40 +00:00
;;
2020-06-05 01:14:05 +00:00
6*) # certificate error
2020-05-26 02:37:55 +00:00
REDIRECTS=0
2020-06-05 01:14:05 +00:00
log d "Not implemented: Client certificates"
# TODO: recheck the speck
die "$((100 + code))" "[$code] $meta"
2020-05-22 13:38:40 +00:00
;;
*)
[[ -z "${code-}" ]] && die 100 "Empty response code."
2020-06-05 01:14:05 +00:00
die "$((100 + code))" "Unknown response code: $code."
;;
esac
}
# GOPHER
# https://tools.ietf.org/html/rfc1436 protocol
# https://tools.ietf.org/html/rfc4266 url
2020-06-08 00:23:14 +00:00
gopher_request() { # gopher_request URL
2020-06-05 01:14:05 +00:00
local url server port type path
url="$1"
port=70
# RFC 4266
[[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]]
server="${BASH_REMATCH[1]}"
port="${BASH_REMATCH[3]:-70}"
type="${BASH_REMATCH[6]:-1}"
path="${BASH_REMATCH[7]}"
log d "URL='$url' SERVER='$server' TYPE='$type' PATH='$path'"
exec 9<>"/dev/tcp/$server/$port"
printf '%s\r\n' "$path" >&9
passthru <&9
}
2020-06-08 00:23:14 +00:00
gopher_response() { # gopher_response URL
2020-06-05 01:14:05 +00:00
local url pre type cur_server
pre=false
url="$1"
# RFC 4266
[[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]]
cur_server="${BASH_REMATCH[1]}"
type="${BASH_REMATCH[6]:-1}"
2020-06-18 13:24:01 +00:00
run history_append "$url" "" # gopher doesn't really have titles, huh
2020-06-05 01:14:05 +00:00
log d "TYPE='$type'"
case "$type" in
0) # text
run display text/plain
;;
1) # menu
run gopher_convert | run display text/gemini
;;
3) # failure
die 203 "GOPHER: failed"
;;
7) # search
if [[ "$url" =~ $'\t' ]]; then
run gopher_convert | run display text/gemini
else
2020-06-07 18:34:59 +00:00
run prompt 'SEARCH'
run blastoff "$url $REPLY"
fi
2020-06-05 01:14:05 +00:00
;;
*) # something else
run download "$url"
;;
2020-05-22 13:38:40 +00:00
esac
}
2020-06-08 00:23:14 +00:00
# 'cat' but in pure bash
2020-06-05 01:14:05 +00:00
passthru() {
while IFS= read -r; do
printf '%s\n' "$REPLY"
done
}
2020-06-08 00:23:14 +00:00
# convert gophermap to text/gemini (probably naive)
2020-06-05 01:14:05 +00:00
gopher_convert() {
local type label path server port regex
# cf. https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk
while IFS= read -r; do
printf -v regex '(.)([^\t]*)(\t([^\t]*)\t([^\t]*)\t([^\t]*))?'
if [[ "$REPLY" =~ $regex ]]; then
type="${BASH_REMATCH[1]}"
label="${BASH_REMATCH[2]}"
path="${BASH_REMATCH[4]:-/}"
server="${BASH_REMATCH[5]:-$cur_server}"
port="${BASH_REMATCH[6]}"
else
log e "CAN'T PARSE LINE"
printf '%s\n' "$REPLY"
continue
fi
case "$type" in
.) # end of file
printf '.\n'
break
;;
i) # label
case "$label" in
'#'* | '*'[[:space:]]*)
if $pre; then
printf '%s\n' '```'
pre=false
fi
;;
*)
if ! $pre; then
printf '%s\n' '```'
pre=true
fi
;;
esac
printf '%s\n' "$label"
;;
h) # html link
if $pre; then
printf '%s\n' '```'
pre=false
fi
printf '=> %s %s\n' "${path:4}" "$label"
;;
T) # telnet link
if $pre; then
printf '%s\n' '```'
pre=false
fi
printf '=> telnet://%s:%s/%s%s %s\n' \
"$server" "$port" "$type" "$path" "$label"
;;
*) # other type
if $pre; then
printf '%s\n' '```'
pre=false
fi
printf '=> gopher://%s:%s/%s%s %s\n' \
"$server" "$port" "$type" "$path" "$label"
;;
esac
done
if $pre; then
printf '%s\n' '```'
fi
# close the connection
exec 9<&-
exec 9>&-
}
2020-06-08 00:23:14 +00:00
# display the fetched content
2020-06-05 01:14:05 +00:00
display() { # display METADATA [TITLE]
local -a less_cmd
local i mime charset
2020-05-31 15:07:03 +00:00
# split header line
local -a hdr
2020-06-05 01:14:05 +00:00
IFS=';' read -ra hdr <<<"$1"
# title is optional but nice looking
local title
if (($# == 2)); then
title="$2"
fi
2020-05-31 15:07:03 +00:00
2020-06-08 00:23:14 +00:00
mime="$(trim_string "${hdr[0],,}")"
2020-05-31 15:58:44 +00:00
for ((i = 1; i <= "${#hdr[@]}"; i++)); do
2020-06-05 01:14:05 +00:00
h="${hdr[$i]}"
2020-05-31 15:07:03 +00:00
case "$h" in
2020-06-05 01:14:05 +00:00
*charset=*) charset="${h#*=}" ;;
2020-05-31 15:07:03 +00:00
esac
done
2020-05-26 02:37:55 +00:00
[[ -z "$mime" ]] && mime="text/gemini"
2020-06-05 01:14:05 +00:00
[[ -z "$charset" ]] && charset="utf-8"
2020-05-31 15:07:03 +00:00
log debug "mime='$mime'; charset='$charset'"
case "$mime" in
2020-05-26 02:37:55 +00:00
text/*)
2020-06-07 23:43:28 +00:00
set_title "$title${title:+ - }bollux"
# render ANSI color escapes and don't wrap pre-formatted blocks
less_cmd=(less -RS)
2020-06-03 12:43:46 +00:00
mklesskey "$BOLLUX_LESSKEY" && less_cmd+=(-k "$BOLLUX_LESSKEY")
2020-06-18 13:24:01 +00:00
local helpline="o:open, g/G:goto, [:back, ]:forward, r:refresh"
2020-05-26 03:18:43 +00:00
less_cmd+=(
2020-06-08 20:20:14 +00:00
-Pm"$(less_prompt_escape "$BOLLUX_URL") - bollux$" # 'status'line
2021-02-26 01:36:14 +00:00
-P="$(less_prompt_escape "$helpline")$" # helpline
-m # start with statusline
+k # float content to the top
2020-05-26 03:18:43 +00:00
)
2020-05-26 02:37:55 +00:00
2020-06-05 01:14:05 +00:00
local typeset
local submime="${mime#*/}"
2020-06-07 23:43:28 +00:00
if declare -Fp "typeset_$submime" &>/dev/null; then
2020-06-05 01:14:05 +00:00
typeset="typeset_$submime"
2020-05-26 02:37:55 +00:00
else
2020-06-05 01:14:05 +00:00
typeset="passthru"
2020-05-26 02:37:55 +00:00
fi
2020-06-05 01:14:05 +00:00
{
run iconv -f "${charset^^}" -t "UTF-8" |
run tee "$BOLLUX_PAGESRC" |
run "$typeset" | #cat
2020-06-05 01:14:05 +00:00
run "${less_cmd[@]}" && bollux_quit
} || run handle_keypress "$?"
2020-05-22 22:12:15 +00:00
;;
2020-05-26 02:37:55 +00:00
*) run download "$BOLLUX_URL" ;;
2020-05-22 22:12:15 +00:00
esac
2020-05-22 20:53:35 +00:00
}
2020-06-08 00:23:14 +00:00
# escape strings for the less prompt
less_prompt_escape() { # less_prompt_escape STRING
2020-06-07 23:43:28 +00:00
local i
for ((i = 0; i < ${#1}; i++)); do
: "${1:i:1}"
case "$_" in
[\?:\.%\\]) printf '\%s' "$_" ;;
*) printf '%s' "$_" ;;
esac
done
printf '\n'
}
2020-06-08 00:23:14 +00:00
# generate a lesskey(1) file for custom keybinds
mklesskey() { # mklesskey FILENAME
2020-05-26 02:37:55 +00:00
lesskey -o "$1" - <<-END
#command
o quit 0 # 48 open a link
g quit 1 # 49 goto a url
[ quit 2 # 50 back
] quit 3 # 51 forward
r quit 4 # 52 re-request / download
2020-06-03 12:43:46 +00:00
G quit 5 # 53 goto a url (pre-filled)
2020-06-02 03:55:40 +00:00
# other keybinds
2020-06-03 12:43:46 +00:00
\\40 forw-screen-force
2020-06-07 22:59:36 +00:00
h left-scroll
l right-scroll
2020-06-07 23:43:28 +00:00
? status # 'status' will show a little help thing.
= noaction
2020-05-26 02:37:55 +00:00
END
}
2020-06-08 00:23:14 +00:00
# normalize files
2020-06-05 01:14:05 +00:00
normalize() {
2020-05-31 13:47:46 +00:00
shopt -s extglob
2020-05-30 21:00:12 +00:00
while IFS= read -r; do
2020-06-08 00:23:14 +00:00
# normalize line endings
2020-05-30 21:00:12 +00:00
printf '%s\n' "${REPLY//$'\r'?($'\n')/}"
2020-05-30 19:35:17 +00:00
done
2020-05-30 22:09:52 +00:00
shopt -u extglob
}
2020-06-08 00:23:14 +00:00
# typeset a text/gemini document
2020-05-26 02:37:55 +00:00
typeset_gemini() {
2020-05-30 21:00:12 +00:00
local pre=false
local ln=0 # link number
if ((T_WIDTH == 0)); then
shopt -s checkwinsize
(
:
:
2020-06-08 16:16:13 +00:00
) # dumb formatting brought to you by shfmt
2020-05-30 21:00:12 +00:00
log d "LINES=$LINES; COLUMNS=$COLUMNS"
T_WIDTH=$COLUMNS
fi
2020-05-30 21:45:56 +00:00
WIDTH=$((T_WIDTH - T_MARGIN))
((WIDTH < 0)) && WIDTH=80 # default if dumb
S_MARGIN=$((T_MARGIN - 1)) # spacing
2020-05-30 21:00:12 +00:00
log d "T_WIDTH=$T_WIDTH"
log d "WIDTH=$WIDTH"
while IFS= read -r; do
case "$REPLY" in
2020-06-05 01:14:05 +00:00
'```'*)
2020-05-30 21:00:12 +00:00
if $pre; then
pre=false
else
pre=true
fi
continue
;;
2020-06-04 04:22:55 +00:00
'=>'*)
2020-05-30 21:00:12 +00:00
: $((ln += 1))
gemini_link "$REPLY" $pre "$ln"
;;
2020-06-04 04:22:55 +00:00
'#'*) gemini_header "$REPLY" $pre ;;
2020-06-05 01:14:05 +00:00
'*'[[:space:]]*)
gemini_list "$REPLY" $pre
2020-05-30 21:45:56 +00:00
;;
'>'*)
gemini_quote "$REPLY" $pre
;;
2020-05-30 21:00:12 +00:00
*) gemini_text "$REPLY" $pre ;;
esac
done
}
gemini_link() {
local re="^(=>)[[:blank:]]*([^[:blank:]]+)[[:blank:]]*(.*)"
2020-06-08 20:19:54 +00:00
local s t a # sigil, text, annotation(url)
local ln="$3"
2020-05-30 21:00:12 +00:00
if ! ${2-false} && [[ "$1" =~ $re ]]; then
s="${BASH_REMATCH[1]}"
a="${BASH_REMATCH[2]}"
t="${BASH_REMATCH[3]}"
if [[ -z "$t" ]]; then
t="$a"
a=
fi
2020-05-30 21:45:56 +00:00
printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
printf "\e[${C_LINK_NUMBER}m[%d]${C_RESET} " "$ln"
fold_line -n -B "\e[${C_LINK_TITLE}m" -A "${C_RESET}" \
-l "$((${#ln} + 3))" -m "${T_MARGIN}" \
"$WIDTH" "$(trim_string "$t")"
fold_line -B " \e[${C_LINK_URL}m" -A "${C_RESET}" \
2020-06-08 20:19:54 +00:00
-l "$((${#ln} + 3 + ${#t}))" -m "$((T_MARGIN + ${#ln} + 2))" \
"$WIDTH" "$a"
2020-05-30 21:00:12 +00:00
else
gemini_pre "$1"
fi
}
gemini_header() {
local re="^(#+)[[:blank:]]*(.*)"
2020-06-08 20:19:54 +00:00
local s t a # sigil, text, annotation(lvl)
2020-05-30 21:00:12 +00:00
if ! ${2-false} && [[ "$1" =~ $re ]]; then
s="${BASH_REMATCH[1]}"
a="${#BASH_REMATCH[1]}"
t="${BASH_REMATCH[2]}"
local hdrfmt
hdrfmt="$(eval echo "\$C_HEADER$a")"
2020-05-30 21:45:56 +00:00
printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
fold_line -B "\e[${hdrfmt}m" -A "${C_RESET}" -m "${T_MARGIN}" \
"$WIDTH" "$t"
2020-05-30 21:00:12 +00:00
else
gemini_pre "$1"
fi
}
gemini_list() {
local re="^(\*)[[:blank:]]*(.*)"
2020-06-08 20:19:54 +00:00
local s t # sigil, text
2020-05-30 21:00:12 +00:00
if ! ${2-false} && [[ "$1" =~ $re ]]; then
s="${BASH_REMATCH[1]}"
t="${BASH_REMATCH[2]}"
printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
fold_line -B "\e[${C_LIST}m" -A "${C_RESET}" -m "$T_MARGIN" \
"$WIDTH" "$t"
2020-05-30 21:00:12 +00:00
else
gemini_pre "$1"
fi
}
gemini_quote() {
local re="^(>)[[:blank:]]*(.*)"
2020-06-08 20:19:54 +00:00
local s t # sigil, text
if ! ${2-false} && [[ "$1" =~ $re ]]; then
s="${BASH_REMATCH[1]}"
t="${BASH_REMATCH[2]}"
printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
fold_line -B "\e[${C_QUOTE}m" -A "${C_RESET}" -m "$T_MARGIN" \
"$WIDTH" "$t"
else
gemini_pre "$1"
fi
}
2020-05-30 21:00:12 +00:00
gemini_text() {
if ! ${2-false}; then
2020-05-30 21:45:56 +00:00
printf "%${S_MARGIN}s " ' '
fold_line -m "$T_MARGIN" \
"$WIDTH" "$1"
2020-05-30 21:00:12 +00:00
else
gemini_pre "$1"
fi
}
gemini_pre() {
2020-05-30 21:45:56 +00:00
printf "\e[${C_SIGIL}m%${S_MARGIN}s " '```'
2020-05-30 21:00:12 +00:00
printf "\e[${C_PRE}m%s${C_RESET}\n" "$1"
}
2020-06-08 00:23:14 +00:00
# wrap lines on words to WIDTH
fold_line() {
# fold_line [-n] [-m MARGIN] [-f MARGIN] [-l LENGTH] [-B BEFORE] [-A AFTER] WIDTH TEXT
local newline=true
local -i margin_all=0 margin_first=0 width ll=0 wl=0 wn=0
local before="" after=""
OPTIND=0
while getopts nm:f:l:B:A: OPT; do
case "$OPT" in
n) # -n = no trailing newline
newline=false
;;
m) # -m MARGIN = margin for all lines
margin_all="$OPTARG"
;;
f) # -f MARGIN = margin for first line
margin_first="$OPTARG"
;;
l) # -l LENGTH = length of line before starting fold
ll="$OPTARG"
;;
B) # -B BEFORE = text to insert before each line
before="$OPTARG"
;;
A) # -A AFTER = text to insert after each line
after="$OPTARG"
;;
*) return 1 ;;
esac
done
shift "$((OPTIND - 1))"
width="$1"
ll=$((ll % width))
#shellcheck disable=2086
set -- $2
local plain=""
if ((margin_first > 0 && ll == 0)); then
printf "%${margin_first}s" " "
fi
if [[ -n "$before" ]]; then
printf '%b' "$before"
2020-05-30 21:00:12 +00:00
fi
for word; do
((wn += 1))
2020-06-08 16:16:13 +00:00
shopt -s extglob
2020-05-30 21:00:12 +00:00
plain="${word//$'\x1b'\[*([0-9;])m/}"
2020-06-08 16:16:13 +00:00
shopt -u extglob
2020-05-30 21:00:12 +00:00
wl=$((${#plain} + 1))
if (((ll + wl) >= width)); then
printf "${after:-}\n%${margin_all}s${before:-}" ' '
2020-05-30 21:00:12 +00:00
ll=$wl
else
((ll += wl))
2020-05-30 21:00:12 +00:00
fi
printf '%s' "$word"
((wn != $#)) && printf ' '
2020-05-30 21:00:12 +00:00
done
[[ -n "$after" ]] && printf '%b' "$after"
$newline && printf '\n'
}
2020-06-08 00:23:14 +00:00
# use the exit code from less (see mklesskey) to do things
handle_keypress() { # handle_keypress CODE
2020-05-26 02:37:55 +00:00
case "$1" in
48) # o - open a link -- show a menu of links on the page
run select_url "$BOLLUX_PAGESRC"
;;
49) # g - goto a url -- input a new url
2020-06-05 01:14:05 +00:00
prompt GO
run blastoff -u "$REPLY"
2020-05-26 02:37:55 +00:00
;;
50) # [ - back in the history
run history_back || {
sleep 0.5
run blastoff "$BOLLUX_URL"
}
2020-05-26 02:37:55 +00:00
;;
51) # ] - forward in the history
run history_forward || {
sleep 0.5
run blastoff "$BOLLUX_URL"
}
2020-05-26 02:37:55 +00:00
;;
52) # r - re-request the current resource
run blastoff "$BOLLUX_URL"
;;
2020-06-03 12:43:46 +00:00
53) # G - goto a url (pre-filled with current)
2020-06-18 13:24:01 +00:00
run prompt -u GO
2020-06-05 01:14:05 +00:00
run blastoff -u "$REPLY"
2020-06-03 12:43:46 +00:00
;;
2020-06-05 01:14:05 +00:00
*) # 54-57 -- still available for binding
die "$?" "less(1) error"
2020-05-26 02:37:55 +00:00
;;
esac
}
2020-06-08 00:23:14 +00:00
# select a URL from a text/gemini file
select_url() { # select_url FILE
2020-05-26 02:37:55 +00:00
run mapfile -t < <(extract_links <"$1")
2020-06-08 20:37:29 +00:00
if ((${#MAPFILE[@]} == 0)); then
log e "No links on this page!"
sleep 0.5
run blastoff "$BOLLUX_URL"
fi
2020-06-04 04:22:55 +00:00
PS3="OPEN> "
2020-05-26 02:37:55 +00:00
select u in "${MAPFILE[@]}"; do
2020-05-31 15:58:44 +00:00
case "$REPLY" in
q) bollux_quit ;;
2020-06-05 01:14:05 +00:00
[^0-9]*) run blastoff -u "$REPLY" && break ;;
2020-05-31 15:58:44 +00:00
esac
2020-06-04 04:22:55 +00:00
run blastoff "${u%%[[:space:]]*}" && break
2020-05-26 02:37:55 +00:00
done </dev/tty
2020-05-23 01:16:32 +00:00
}
2020-06-08 00:23:14 +00:00
# extract the links from a text/gemini file
2020-05-26 02:37:55 +00:00
extract_links() {
2020-06-03 02:07:46 +00:00
local url alt
2020-06-04 04:22:55 +00:00
while read -r; do
if [[ "$REPLY" =~ ^=\>[[:space:]]*([^[:space:]]+)([[:space:]]+(.*))?$ ]]; then
2020-06-03 02:07:46 +00:00
url="${BASH_REMATCH[1]}"
alt="${BASH_REMATCH[3]}"
if [[ "$alt" ]]; then
printf '%s \e[34m(%s)\e[0m\n' "$url" "$alt"
else
printf '%s\n' "$url"
fi
fi
done
}
2020-06-08 00:23:14 +00:00
# download $BOLLUX_URL
2020-05-26 02:37:55 +00:00
download() {
tn="$(mktemp)"
log x "Downloading: '$BOLLUX_URL' => '$tn'..."
dd status=progress >"$tn"
fn="$BOLLUX_DOWNDIR/${BOLLUX_URL##*/}"
if [[ -f "$fn" ]]; then
log x "Saved '$tn'."
elif mv "$tn" "$fn"; then
log x "Saved '$fn'."
else
log error "Error saving '$fn': downloaded to '$tn'."
fi
2020-05-23 01:16:32 +00:00
}
2020-06-08 00:23:14 +00:00
# initialize bollux
2020-06-05 01:14:05 +00:00
bollux_init() {
# Trap cleanup
trap bollux_cleanup INT QUIT EXIT
# State
REDIRECTS=0
set -f
# History
declare -a HISTORY # history is kept in an array
HN=0 # position of history in the array
run mkdir -p "${BOLLUX_HISTFILE%/*}"
}
2020-06-08 00:23:14 +00:00
# clean up on exit
2020-06-05 01:14:05 +00:00
bollux_cleanup() {
2020-06-07 19:02:57 +00:00
# Stubbed in case of need in future
2020-06-05 01:14:05 +00:00
:
}
2020-06-08 00:23:14 +00:00
# append a URL to history
history_append() { # history_append URL TITLE
BOLLUX_URL="$1"
# date/time, url, title (best guess)
run printf '%(%FT%T)T\t%s\t%s\n' -1 "$1" "$2" >>"$BOLLUX_HISTFILE"
HISTORY[$HN]="$BOLLUX_URL"
((HN += 1))
}
2020-06-08 00:23:14 +00:00
# move back in history (session)
history_back() {
log d "HN=$HN"
((HN -= 2))
2020-06-01 20:36:33 +00:00
if ((HN < 0)); then
HN=0
log e "Beginning of history."
return 1
fi
run blastoff "${HISTORY[$HN]}"
}
2020-06-05 01:14:05 +00:00
2020-06-08 00:23:14 +00:00
# move forward in history (session)
history_forward() {
log d "HN=$HN"
if ((HN >= ${#HISTORY[@]})); then
HN="${#HISTORY[@]}"
log e "End of history."
return 1
fi
run blastoff "${HISTORY[$HN]}"
}
2020-05-22 20:53:35 +00:00
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
2020-05-26 02:37:55 +00:00
run bollux "$@"
2020-05-22 20:53:35 +00:00
fi