2 # bollux: a bash gemini client
3 # Author: Case Duckworth
15 $PRGN (v. $VRSN): a bash gemini client
20 -h show this help and exit
21 -q be quiet: log no messages
22 -v verbose: log more messages
24 URL the URL to start in
25 If not provided, the user will be prompted.
41 trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
44 [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
47 [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return
56 printf >&2 '\e[%sm%s:\e[0m\t%s\n' "$fmt" "$PRGN" "$*"
64 if [[ ! "${BOLLUX_URL:+isset}" ]]; then
65 run prompt GO BOLLUX_URL
68 run blastoff "$BOLLUX_URL"
72 while getopts :hvq OPT; do
78 v) BOLLUX_LOGLEVEL=DEBUG ;;
79 q) BOLLUX_LOGLEVEL=QUIET ;;
80 :) die 1 "Option -$OPTARG requires an argument" ;;
81 *) die 1 "Unknown option: -$OPTARG" ;;
91 : "${BOLLUX_CONFIG:=${XDG_CONFIG_DIR:-$HOME/.config}/bollux/config}"
93 if [ -f "$BOLLUX_CONFIG" ]; then
94 # shellcheck disable=1090
97 log debug "Can't load config file '$BOLLUX_CONFIG'."
100 : "${BOLLUX_DOWNDIR:=.}" # where to save downloads
101 : "${BOLLUX_LOGLEVEL:=3}" # log level
102 : "${BOLLUX_MAXREDIR:=5}" # max redirects
103 : "${BOLLUX_PORT:=1965}" # port number
104 : "${BOLLUX_PROTO:=gemini}" # default protocol
105 : "${BOLLUX_LESSKEY:=/tmp/bollux-lesskey}" # where to store binds
106 : "${BOLLUX_PAGESRC:=/tmp/bollux-src}" # where to save the page source
107 : "${BOLLUX_URL:=}" # start url
113 read </dev/tty -e -r -p "$prompt> " "$@"
116 blastoff() { # load a url
117 local well_formed=true
118 if [[ "$1" == "-u" ]]; then
124 if $well_formed && [[ "$1" != "$BOLLUX_URL" ]]; then
125 URL="$(run munge_url "$1" "$BOLLUX_URL")"
127 [[ "$URL" != *://* ]] && URL="$BOLLUX_PROTO://$URL"
128 URL="$(trim <<<"$URL")"
131 server="${server%%/*}"
133 run request_url "$server" "$BOLLUX_PORT" "$URL" |
134 run handle_response "$URL"
139 eval "$(split_url new <<<"$1")"
140 for k in "${!new[@]}"; do log d "new[$k]=${new[$k]}"; done
141 eval "$(split_url old <<<"$2")"
142 for k in "${!old[@]}"; do log d "old[$k]=${old[$k]}"; done
144 u['scheme']="${new['scheme']:-${old['scheme']:-}}"
145 u['authority']="${new['authority']:-${old['authority']:-}}"
146 # XXX this whole path thing is wack
147 if [[ "${new['path']+isset}" ]]; then
149 if [[ "${new['path']}" == /* ]]; then
150 log d 'new path == /*'
151 u['path']="${new['path']}"
152 elif [[ "${new['authority']}" == "${old['authority']}" || ! "${new['authority']+isset}" ]]; then
153 p="${old['path']:-}/${new['path']}"
154 log d "$p ( $(normalize_path <<<"$p") )"
155 u['path']="$(normalize_path <<<"$p")"
157 log d 'u path = new path'
158 u['path']="${new['path']}"
160 elif [[ "${new['query']+isset}" || "${new['fragment']+isset}" ]]; then
161 log d 'u path = old path'
162 u['path']="${old['path']}"
166 u['query']="${new['query']:-}"
167 u['fragment']="${new['fragment']:-}"
168 for k in "${!u[@]}"; do log d "u[$k]=${u[$k]}"; done
170 run printf '%s%s%s%s%s\n' \
171 "${u['scheme']}" "${u['authority']}" "${u['path']}" \
172 "${u['query']}" "${u['fragment']}"
177 split($0, path, /\//)
179 if (path[c] == "" || path[c] == ".") {
182 if (path[c] == "..") {
183 sub(/[^\/]+$/, "", ret)
186 if (! ret || match(ret, /\/$/)) {
191 ret = ret slash path[c]
193 print (ret ~ /^\// ? "" : "/") ret
199 if (match($0, /^[A-Za-z]+:/)) {
200 arr["scheme"] = substr($0, RSTART, RLENGTH)
201 $0 = substr($0, RLENGTH + 1)
203 if (match($0, /^\/\/[^\/?#]+?/) || (match($0, /^[^\/?#]+?/) && scheme)) {
204 arr["authority"] = substr($0, RSTART, RLENGTH)
205 $0 = substr($0, RLENGTH + 1)
207 if (match($0, /^\/?[^?#]+/)) {
208 arr["path"] = substr($0, RSTART, RLENGTH)
209 $0 = substr($0, RLENGTH + 1)
211 if (match($0, /^\?[^#]+/)) {
212 arr["query"] = substr($0, RSTART, RLENGTH)
213 $0 = substr($0, RLENGTH + 1)
215 if (match($0, /^#.*/)) {
216 arr["fragment"] = substr($0, RSTART, RLENGTH)
217 $0 = substr($0, RLENGTH + 1)
220 sub(/[[:space:]]+$/, "", arr[part])
221 printf var "[\"%s\"]=\"%s\"\n", part, arr[part]
231 ssl_cmd=(openssl s_client -crlf -quiet -connect "$server:$port")
232 ssl_cmd+=(-servername "$server") # SNI
233 run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null
237 local url="$1" code meta
239 while read -r -d $'\r' hdr; do
240 code="$(gawk '{print $1}' <<<"$hdr")"
242 gawk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$hdr"
247 log x "[$code] $meta"
253 run prompt "$meta" QUERY
254 run blastoff "?$QUERY"
263 if ((REDIRECTS > BOLLUX_MAXREDIR)); then
264 die $((100 + code)) "Too many redirects!"
271 die "$((100 + code))" "$code"
275 die "$((100 + code))" "$code"
279 die "$((100 + code))" "$code"
281 *) die "$((100 + code)) Unknown response code: $code." ;;
288 mime="$(cut -d\; -f1 <<<"$1" | trim)"
289 charset="$(cut -d\; -f2 <<<"$1" | trim)"
291 *) mime="$(trim <<<"$1")" ;;
294 [[ -z "$mime" ]] && mime="text/gemini"
295 if [[ -z "$charset" ]]; then
298 charset="${charset#charset=}"
301 log debug "mime=$mime; charset=$charset"
307 [[ -r "$BOLLUX_LESSKEY" ]] || mklesskey "$BOLLUX_LESSKEY"
308 } && less_cmd+=(-k "$BOLLUX_LESSKEY")
311 -PM'o\:open, g\:goto, r\:refresh$'
316 if declare -F | grep -q "$submime"; then
317 log d "typeset_$submime"
320 run "typeset_$submime" |
321 tee "$BOLLUX_PAGESRC" |
323 } || run handle_keypress "$?"
328 tee "$BOLLUX_PAGESRC" |
330 } || run handle_keypress "$?"
333 *) run download "$BOLLUX_URL" ;;
338 lesskey -o "$1" - <<-END
340 o quit 0 # 48 open a link
341 g quit 1 # 49 goto a url
343 ] quit 3 # 51 forward
344 r quit 4 # 52 re-request / download
349 gawk 'BEGIN{RS="\n\n"}{gsub(/\r\n?/,"\n");print;print ""}'
355 /^###/ { sub(/^#+[[:space:]]*/, "");
356 printf "### \033[3m%s\033[0m\n", $0
358 /^##/ { sub(/^#+[[:space:]]*/, "");
359 printf "## \033[1m%s\033[0m\n", $0
361 /^#/ { sub(/^#+[[:space:]]*/, "");
362 printf "# \033[1;4m%s\033[0m\n", $0
365 sub(/=>[[:space:]]*/, "")
368 desc = desc (desc?" ":"") $w
369 printf "=> \033[1m[%02d]\033[0m \033[4m%s\033[0m\t\033[36m%s\033[0m\n",
372 /```/ { pre = !pre; next }
373 pre { printf "``` %s\n", $0; next }
374 # /^\*/ { sub(/\*[[:space:]]*/, ""); }
375 { sub(/^/, " "); print }
381 48) # o - open a link -- show a menu of links on the page
382 run select_url "$BOLLUX_PAGESRC"
384 49) # g - goto a url -- input a new url
386 run blastoff -u "$URL"
388 50) # [ - back in the history
391 51) # ] - forward in the history
394 52) # r - re-request the current resource
395 run blastoff "$BOLLUX_URL"
397 *) # 53-57 -- still available for binding
403 run mapfile -t < <(extract_links <"$1")
404 select u in "${MAPFILE[@]}"; do
405 run blastoff "$(gawk '{print $1}' <<<"$u")" && break
410 gawk -F$'\t' '/^=>/ {
411 gsub("\033\\[[^m]*m", "")
412 sub(/=>[[:space:]]*\[[0-9]+\][[:space:]]*/,"")
414 printf "%s (\033[34m%s\033[0m)\n", $2, $1
422 log x "Downloading: '$BOLLUX_URL' => '$tn'..."
423 dd status=progress >"$tn"
424 fn="$BOLLUX_DOWNDIR/${BOLLUX_URL##*/}"
425 if [[ -f "$fn" ]]; then
427 elif mv "$tn" "$fn"; then
430 log error "Error saving '$fn': downloaded to '$tn'."
434 history_back() { log error "Not implemented."; }
435 history_forward() { log error "Not implemented."; }
437 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
440 BOLLUX_LOGLEVEL=DEBUG