#!/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 <&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 " "$@" } 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 munge_url "$1" "$BOLLUX_URL")" fi [[ "$URL" != *://* ]] && URL="$BOLLUX_PROTO://$URL" URL="$(trim <<<"$URL")" server="${URL#*://}" server="${server%%/*}" run request_url "$server" "$BOLLUX_PORT" "$URL" | run handle_response "$URL" } munge_url() { local -A new old u eval "$(split_url new <<<"$1")" for k in "${!new[@]}"; do log d "new[$k]=${new[$k]}"; done eval "$(split_url old <<<"$2")" for k in "${!old[@]}"; do log d "old[$k]=${old[$k]}"; done u['scheme']="${new['scheme']:-${old['scheme']:-}}" u['authority']="${new['authority']:-${old['authority']:-}}" # XXX this whole path thing is wack if [[ "${new['path']+isset}" ]]; then log d 'new path set' if [[ "${new['path']}" == /* ]]; then log d 'new path == /*' u['path']="${new['path']}" elif [[ "${new['authority']}" == "${old['authority']}" || ! "${new['authority']+isset}" ]]; then p="${old['path']:-}/${new['path']}" log d "$p ( $(normalize_path <<<"$p") )" u['path']="$(normalize_path <<<"$p")" else log d 'u path = new path' u['path']="${new['path']}" fi elif [[ "${new['query']+isset}" || "${new['fragment']+isset}" ]]; then log d 'u path = old path' u['path']="${old['path']}" else u['path']="/" fi u['query']="${new['query']:-}" u['fragment']="${new['fragment']:-}" for k in "${!u[@]}"; do log d "u[$k]=${u[$k]}"; done run printf '%s%s%s%s%s\n' \ "${u['scheme']}" "${u['authority']}" "${u['path']}" \ "${u['query']}" "${u['fragment']}" } normalize_path() { gawk '{ split($0, path, /\//) for (c in path) { if (path[c] == "" || path[c] == ".") { continue } if (path[c] == "..") { sub(/[^\/]+$/, "", ret) continue } if (! ret || match(ret, /\/$/)) { slash = "" } else { slash = "/" } ret = ret slash path[c] } print (ret ~ /^\// ? "" : "/") ret }' } split_url() { gawk -vvar="$1" '{ if (match($0, /^[A-Za-z]+:/)) { arr["scheme"] = substr($0, RSTART, RLENGTH) $0 = substr($0, RLENGTH + 1) } if (match($0, /^\/\/[^\/?#]+?/) || (match($0, /^[^\/?#]+?/) && scheme)) { arr["authority"] = substr($0, RSTART, RLENGTH) $0 = substr($0, RLENGTH + 1) } if (match($0, /^\/?[^?#]+/)) { arr["path"] = substr($0, RSTART, RLENGTH) $0 = substr($0, RLENGTH + 1) } if (match($0, /^\?[^#]+/)) { arr["query"] = substr($0, RSTART, RLENGTH) $0 = substr($0, RLENGTH + 1) } if (match($0, /^#.*/)) { arr["fragment"] = substr($0, RSTART, RLENGTH) $0 = substr($0, RLENGTH + 1) } for (part in arr) { sub(/[[:space:]]+$/, "", arr[part]) printf var "[\"%s\"]=\"%s\"\n", part, arr[part] } }' } 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 | run "typeset_$submime" | tee "$BOLLUX_PAGESRC" | 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 } /^###/ { sub(/^#+[[:space:]]*/, ""); printf "### \033[3m%s\033[0m\n", $0 next } /^##/ { sub(/^#+[[:space:]]*/, ""); printf "## \033[1m%s\033[0m\n", $0 next } /^#/ { sub(/^#+[[:space:]]*/, ""); printf "# \033[1;4m%s\033[0m\n", $0 next } /^=>/ { sub(/=>[[:space:]]*/, "") url = $1; desc = "" for (w=2;w<=NF;w++) desc = desc (desc?" ":"") $w printf "=> \033[1m[%02d]\033[0m \033[4m%s\033[0m\t\033[36m%s\033[0m\n", (++ln), desc, url next } /```/ { pre = !pre; next } pre { printf "``` %s\n", $0; next } # /^\*/ { sub(/\*[[:space:]]*/, ""); } { sub(/^/, " "); print } ' } 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 / { gsub("\033\\[[^m]*m", "") sub(/=>[[:space:]]*\[[0-9]+\][[:space:]]*/,"") if ($2) printf "%s (\033[34m%s\033[0m)\n", $2, $1 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