518 lines
11 KiB
Bash
Executable File
518 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# bollux: a bash gemini client
|
|
# Author: Case Duckworth
|
|
# License: MIT
|
|
# Version: 0.1
|
|
|
|
# Program information
|
|
PRGN="${0##*/}"
|
|
VRSN=0.1
|
|
# State
|
|
REDIRECTS=0
|
|
|
|
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
|
|
}
|
|
|
|
run() {
|
|
log debug "$@"
|
|
"$@"
|
|
}
|
|
|
|
die() {
|
|
ec="$1"
|
|
shift
|
|
log error "$*"
|
|
exit "$ec"
|
|
}
|
|
|
|
trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
|
|
|
|
log() {
|
|
[[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
|
|
case "$1" in
|
|
d* | D*) # debug
|
|
[[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return
|
|
fmt=34
|
|
;;
|
|
e* | E*) # error
|
|
fmt=31
|
|
;;
|
|
*) fmt=1 ;;
|
|
esac
|
|
shift
|
|
printf >&2 '\e[%sm%s:\e[0m\t%s\n' "$fmt" "$PRGN" "$*"
|
|
}
|
|
|
|
# main entry point
|
|
bollux() {
|
|
run bollux_args "$@"
|
|
run bollux_config
|
|
|
|
if [[ ! "${BOLLUX_URL:+isset}" ]]; then
|
|
run prompt GO BOLLUX_URL
|
|
fi
|
|
|
|
run blastoff "$BOLLUX_URL"
|
|
}
|
|
|
|
bollux_args() {
|
|
while getopts :hvq OPT; do
|
|
case "$OPT" in
|
|
h)
|
|
bollux_usage
|
|
exit
|
|
;;
|
|
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
|
|
}
|
|
|
|
bollux_config() {
|
|
: "${BOLLUX_CONFIG:=${XDG_CONFIG_DIR:-$HOME/.config}/bollux/config}"
|
|
|
|
if [ -f "$BOLLUX_CONFIG" ]; then
|
|
# shellcheck disable=1090
|
|
. "$BOLLUX_CONFIG"
|
|
else
|
|
log debug "Can't load config file '$BOLLUX_CONFIG'."
|
|
fi
|
|
|
|
: "${BOLLUX_DOWNDIR:=.}" # where to save downloads
|
|
: "${BOLLUX_LOGLEVEL:=3}" # log level
|
|
: "${BOLLUX_MAXREDIR:=5}" # max redirects
|
|
: "${BOLLUX_PORT:=1965}" # port number
|
|
: "${BOLLUX_PROTO:=gemini}" # default protocol
|
|
: "${BOLLUX_LESSKEY:=/tmp/bollux-lesskey}" # where to store binds
|
|
: "${BOLLUX_PAGESRC:=/tmp/bollux-src}" # where to save the page source
|
|
: "${BOLLUX_URL:=}" # start url
|
|
}
|
|
|
|
prompt() {
|
|
prompt="$1"
|
|
shift
|
|
read </dev/tty -e -r -p "$prompt> " "$@"
|
|
}
|
|
|
|
blastoff() { # load a url
|
|
local well_formed=true
|
|
if [[ "$1" == "-u" ]]; then
|
|
well_formed=false
|
|
shift
|
|
fi
|
|
URL="$1"
|
|
|
|
if $well_formed && [[ "$1" != "$BOLLUX_URL" ]]; then
|
|
URL="$(run transform_resource "$BOLLUX_URL" "$1")"
|
|
fi
|
|
[[ "$URL" != *://* ]] && URL="$BOLLUX_PROTO://$URL"
|
|
URL="$(trim <<<"$URL")"
|
|
|
|
server="${URL#*://}"
|
|
server="${server%%/*}"
|
|
|
|
run request_url "$server" "$BOLLUX_PORT" "$URL" |
|
|
run handle_response "$URL"
|
|
}
|
|
|
|
transform_resource() { # transform_resource BASE_URL REFERENCE_URL
|
|
declare -A R B T # reference, base url, target
|
|
eval "$(parse_url B "$1")"
|
|
eval "$(parse_url R "$2")"
|
|
# A non-strict parser may ignore a scheme in the reference
|
|
# if it is identical to the base URI's scheme.
|
|
if ! "${STRICT:-true}" && [[ "${R[scheme]}" == "${B[scheme]}" ]]; then
|
|
unset "${R[scheme]}"
|
|
fi
|
|
|
|
# basically pseudo-code from spec ported to bash
|
|
if isdefined "R[scheme]"; then
|
|
T[scheme]="${R[scheme]}"
|
|
isdefined "R[authority]" && T[authority]="${R[authority]}"
|
|
isdefined R[path] &&
|
|
T[path]="$(remove_dot_segments "${R[path]}")"
|
|
isdefined "R[query]" && T[query]="${R[query]}"
|
|
else
|
|
if isdefined "R[authority]"; then
|
|
T[authority]="${R[authority]}"
|
|
isdefined "R[authority]" &&
|
|
T[path]="$(remove_dot_segments "${R[path]}")"
|
|
isdefined R[query] && T[query]="${R[query]}"
|
|
else
|
|
if isempty "R[path]"; then
|
|
T[path]="${B[path]}"
|
|
if isdefined R[query]; then
|
|
T[query]="${R[query]}"
|
|
else
|
|
T[query]="${B[query]}"
|
|
fi
|
|
else
|
|
if [[ "${R[path]}" == /* ]]; then
|
|
T[path]="$(remove_dot_segments "${R[path]}")"
|
|
else
|
|
T[path]="$(merge_paths "B[authority]" "${B[path]}" "${R[path]}")"
|
|
T[path]="$(remove_dot_segments "${T[path]}")"
|
|
fi
|
|
isdefined R[query] && T[query]="${R[query]}"
|
|
fi
|
|
T[authority]="${B[authority]}"
|
|
fi
|
|
T[scheme]="${B[scheme]}"
|
|
fi
|
|
isdefined R[fragment] && T[fragment]="${R[fragment]}"
|
|
# cf. 5.3 -- recomposition
|
|
local r=""
|
|
isdefined "T[scheme]" && r="$r${T[scheme]}:"
|
|
isdefined "T[authority]" && r="$r//${T[authority]}"
|
|
r="$r${T[path]}"
|
|
isdefined T[query] && r="$r?${T[query]}"
|
|
isdefined T[fragment] && r="$r#${T[fragment]}"
|
|
printf '%s\n' "$r"
|
|
}
|
|
|
|
merge_paths() { # 5.2.3
|
|
# shellcheck disable=2034
|
|
B_authority="$1"
|
|
B_path="$2"
|
|
R_path="$3"
|
|
# if R_path is empty, get rid of // in B_path
|
|
if [[ -z "$R_path" ]]; then
|
|
printf '%s\n' "${B_path//\/\//\//}"
|
|
return
|
|
fi
|
|
|
|
if isdefined "B_authority" && isempty "B_path"; then
|
|
printf '/%s\n' "${R_path//\/\//\//}"
|
|
else
|
|
if [[ "$B_path" == */* ]]; then
|
|
B_path="${B_path%/*}/"
|
|
else
|
|
B_path=""
|
|
fi
|
|
printf '%s/%s\n' "${B_path%/}" "${R_path#/}"
|
|
fi
|
|
}
|
|
|
|
remove_dot_segments() { # 5.2.4
|
|
local input="$1"
|
|
local output=
|
|
# ^/\.(/|$) - BASH_REMATCH[0]
|
|
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
|
|
[[ $input =~ ^(/?[^/]*)(/?.*)$ ]] || echo NOMATCH >&2
|
|
output="$output${BASH_REMATCH[1]}"
|
|
input="${BASH_REMATCH[2]}"
|
|
fi
|
|
done
|
|
printf '%s\n' "${output//\/\//\//}"
|
|
}
|
|
|
|
parse_url() { # eval "$(split_url NAME STRING)" => NAME[...]
|
|
local name="$1"
|
|
local string="$2"
|
|
local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
|
|
[[ $string =~ $re ]] || return $?
|
|
|
|
local scheme="${BASH_REMATCH[2]}"
|
|
local authority="${BASH_REMATCH[4]}"
|
|
local path="${BASH_REMATCH[5]}"
|
|
local query="${BASH_REMATCH[7]}"
|
|
local fragment="${BASH_REMATCH[9]}"
|
|
|
|
for c in scheme authority query fragment; do
|
|
[[ "${!c}" ]] &&
|
|
printf '%s[%s]=%q\n' "$name" "$c" "${!c}"
|
|
done
|
|
# unclear if the path is always set even if empty but it looks that way
|
|
printf '%s[path]=%q\n' "$name" "$path"
|
|
}
|
|
|
|
# is a NAME defined ('set' in bash)?
|
|
isdefined() { [[ "${!1+x}" ]]; } # isdefined NAME
|
|
# is a NAME defined AND empty?
|
|
isempty() { [[ ! "${!1-x}" ]]; } # isempty NAME
|
|
|
|
request_url() {
|
|
local server="$1"
|
|
local port="$2"
|
|
local url="$3"
|
|
|
|
ssl_cmd=(openssl s_client -crlf -quiet -connect "$server:$port")
|
|
ssl_cmd+=(-servername "$server") # SNI
|
|
run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null
|
|
}
|
|
|
|
handle_response() {
|
|
local url="$1" code meta
|
|
|
|
while read -r -d $'\r' hdr; do
|
|
code="$(gawk '{print $1}' <<<"$hdr")"
|
|
meta="$(
|
|
gawk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$hdr"
|
|
)"
|
|
break
|
|
done
|
|
|
|
log x "[$code] $meta"
|
|
|
|
case "$code" in
|
|
1*)
|
|
REDIRECTS=0
|
|
BOLLUX_URL="$URL"
|
|
run prompt "$meta" QUERY
|
|
run blastoff "?$QUERY"
|
|
;;
|
|
2*)
|
|
REDIRECTS=0
|
|
BOLLUX_URL="$URL"
|
|
run display "$meta"
|
|
;;
|
|
3*)
|
|
((REDIRECTS += 1))
|
|
if ((REDIRECTS > BOLLUX_MAXREDIR)); then
|
|
die $((100 + code)) "Too many redirects!"
|
|
fi
|
|
BOLLUX_URL="$URL"
|
|
run blastoff "$meta"
|
|
;;
|
|
4*)
|
|
REDIRECTS=0
|
|
die "$((100 + code))" "$code"
|
|
;;
|
|
5*)
|
|
REDIRECTS=0
|
|
die "$((100 + code))" "$code"
|
|
;;
|
|
6*)
|
|
REDIRECTS=0
|
|
die "$((100 + code))" "$code"
|
|
;;
|
|
*) die "$((100 + code)) Unknown response code: $code." ;;
|
|
esac
|
|
}
|
|
|
|
display() {
|
|
case "$1" in
|
|
*\;*)
|
|
mime="$(cut -d\; -f1 <<<"$1" | trim)"
|
|
charset="$(cut -d\; -f2 <<<"$1" | trim)"
|
|
;;
|
|
*) mime="$(trim <<<"$1")" ;;
|
|
esac
|
|
|
|
[[ -z "$mime" ]] && mime="text/gemini"
|
|
if [[ -z "$charset" ]]; then
|
|
charset="utf-8"
|
|
else
|
|
charset="${charset#charset=}"
|
|
fi
|
|
|
|
log debug "mime=$mime; charset=$charset"
|
|
|
|
case "$mime" in
|
|
text/*)
|
|
less_cmd=(less -R)
|
|
{
|
|
[[ -r "$BOLLUX_LESSKEY" ]] || mklesskey "$BOLLUX_LESSKEY"
|
|
} && less_cmd+=(-k "$BOLLUX_LESSKEY")
|
|
less_cmd+=(
|
|
-Pm'bollux$'
|
|
-PM'o\:open, g\:goto, r\:refresh$'
|
|
-M
|
|
)
|
|
|
|
submime="${mime#*/}"
|
|
if declare -F | grep -q "$submime"; then
|
|
log d "typeset_$submime"
|
|
{
|
|
normalize_crlf |
|
|
tee "$BOLLUX_PAGESRC" |
|
|
run "typeset_$submime" |
|
|
run "${less_cmd[@]}"
|
|
} || run handle_keypress "$?"
|
|
else
|
|
log "cat"
|
|
{
|
|
normalize_crlf |
|
|
tee "$BOLLUX_PAGESRC" |
|
|
run "${less_cmd[@]}"
|
|
} || run handle_keypress "$?"
|
|
fi
|
|
;;
|
|
*) run download "$BOLLUX_URL" ;;
|
|
esac
|
|
}
|
|
|
|
mklesskey() {
|
|
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
|
|
END
|
|
}
|
|
|
|
normalize_crlf() {
|
|
gawk 'BEGIN{RS="\n\n"}{gsub(/\r\n?/,"\n");print;print ""}'
|
|
}
|
|
|
|
typeset_gemini() {
|
|
gawk '
|
|
BEGIN {
|
|
pre = 0
|
|
margin = margin ? margin : 4
|
|
txs = ""
|
|
lns = "\033[1m"
|
|
lus = "\033[36m"
|
|
lts = "\033[4m"
|
|
pfs = ""
|
|
h1s = "\033[1;4m"
|
|
h2s = "\033[1m"
|
|
h3s = "\033[3m"
|
|
lis = ""
|
|
res = "\033[0m"
|
|
ms = "\033[35m"
|
|
}
|
|
/```/ {
|
|
pre = ! pre
|
|
next
|
|
}
|
|
pre {
|
|
mark = "```"
|
|
fmt = pfs "%s" res
|
|
text = $0
|
|
}
|
|
/^#/ {
|
|
match($0, /#+/)
|
|
mark = substr($0, RSTART, RLENGTH)
|
|
sub(/#+[[:space:]]*/, "", $0)
|
|
level = length(mark)
|
|
if (level == 1) {
|
|
fmt = h1s "%s" res
|
|
} else if (level == 2) {
|
|
fmt = h2s "%s" res
|
|
} else {
|
|
fmt = h3s "%s" res
|
|
}
|
|
}
|
|
/^=>/ {
|
|
mark = "=>"
|
|
sub(/=>[[:space:]]*/, "", $0)
|
|
desc = $1
|
|
text = ""
|
|
for (w = 2; w <= NF; w++) {
|
|
text = text (text ? " " : "") $w
|
|
}
|
|
fmt = lns "[" (++ln) "]" res " " lts "%s" res "\t" lus "%s" res
|
|
}
|
|
/^\*[[:space:]]/ {
|
|
mark = "*"
|
|
sub(/\*[[:space:]]*/, "", $0)
|
|
fmt = lis "%s" res
|
|
}
|
|
{
|
|
mark = mark ? mark : mark
|
|
fmt = fmt ? fmt : "%s"
|
|
text = text ? text : $0
|
|
desc = desc ? desc : ""
|
|
printf ms "%" (margin-1) "s " res fmt "\n", mark, text, desc
|
|
mark = fmt = text = desc = ""
|
|
}
|
|
'
|
|
}
|
|
|
|
handle_keypress() {
|
|
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
|
|
prompt GO URL
|
|
run blastoff -u "$URL"
|
|
;;
|
|
50) # [ - back in the history
|
|
run history_back
|
|
;;
|
|
51) # ] - forward in the history
|
|
run history_forward
|
|
;;
|
|
52) # r - re-request the current resource
|
|
run blastoff "$BOLLUX_URL"
|
|
;;
|
|
*) # 53-57 -- still available for binding
|
|
;;
|
|
esac
|
|
}
|
|
|
|
select_url() {
|
|
run mapfile -t < <(extract_links <"$1")
|
|
select u in "${MAPFILE[@]}"; do
|
|
run blastoff "$(gawk '{print $1}' <<<"$u")" && break
|
|
done </dev/tty
|
|
}
|
|
|
|
extract_links() {
|
|
gawk -F$'\t' '
|
|
/^=>/ {
|
|
sub(/=>[[:space:]]*/,"")
|
|
if ($2)
|
|
printf "%s (\033[34m%s\033[0m)\n", $1, $2
|
|
else
|
|
printf "%s\n", $1
|
|
}'
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
history_back() { log error "Not implemented."; }
|
|
history_forward() { log error "Not implemented."; }
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
run bollux "$@"
|
|
else
|
|
BOLLUX_LOGLEVEL=DEBUG
|
|
fi
|