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 transform_resource "$BOLLUX_URL" "$1")"
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"
137 transform_resource() { # transform_resource BASE_URL REFERENCE_URL
138 declare -A R B T # reference, base url, target
139 eval "$(parse_url B "$1")"
140 eval "$(parse_url R "$2")"
141 # A non-strict parser may ignore a scheme in the reference
142 # if it is identical to the base URI's scheme.
143 if ! "${STRICT:-true}" && [[ "${R[scheme]}" == "${B[scheme]}" ]]; then
147 # basically pseudo-code from spec ported to bash
148 if isdefined "R[scheme]"; then
149 T[scheme]="${R[scheme]}"
150 isdefined "R[authority]" && T[authority]="${R[authority]}"
152 T[path]="$(remove_dot_segments "${R[path]}")"
153 isdefined "R[query]" && T[query]="${R[query]}"
155 if isdefined "R[authority]"; then
156 T[authority]="${R[authority]}"
157 isdefined "R[authority]" &&
158 T[path]="$(remove_dot_segments "${R[path]}")"
159 isdefined R[query] && T[query]="${R[query]}"
161 if isempty "R[path]"; then
163 if isdefined R[query]; then
164 T[query]="${R[query]}"
166 T[query]="${B[query]}"
169 if [[ "${R[path]}" == /* ]]; then
170 T[path]="$(remove_dot_segments "${R[path]}")"
172 T[path]="$(merge_paths "B[authority]" "${B[path]}" "${R[path]}")"
173 T[path]="$(remove_dot_segments "${T[path]}")"
175 isdefined R[query] && T[query]="${R[query]}"
177 T[authority]="${B[authority]}"
179 T[scheme]="${B[scheme]}"
181 isdefined R[fragment] && T[fragment]="${R[fragment]}"
182 # cf. 5.3 -- recomposition
184 isdefined "T[scheme]" && r="$r${T[scheme]}:"
185 isdefined "T[authority]" && r="$r//${T[authority]}"
187 isdefined T[query] && r="$r?${T[query]}"
188 isdefined T[fragment] && r="$r#${T[fragment]}"
192 merge_paths() { # 5.2.3
193 # shellcheck disable=2034
197 # if R_path is empty, get rid of // in B_path
198 if [[ -z "$R_path" ]]; then
199 printf '%s\n' "${B_path//\/\//\//}"
203 if isdefined "B_authority" && isempty "B_path"; then
204 printf '/%s\n' "${R_path//\/\//\//}"
206 if [[ "$B_path" == */* ]]; then
207 B_path="${B_path%/*}/"
211 printf '%s/%s\n' "${B_path%/}" "${R_path#/}"
215 remove_dot_segments() { # 5.2.4
218 # ^/\.(/|$) - BASH_REMATCH[0]
219 while [[ "$input" ]]; do
220 if [[ "$input" =~ ^\.\.?/ ]]; then
221 input="${input#${BASH_REMATCH[0]}}"
222 elif [[ "$input" =~ ^/\.(/|$) ]]; then
223 input="/${input#${BASH_REMATCH[0]}}"
224 elif [[ "$input" =~ ^/\.\.(/|$) ]]; then
225 input="/${input#${BASH_REMATCH[0]}}"
226 [[ "$output" =~ /?[^/]+$ ]]
227 output="${output%${BASH_REMATCH[0]}}"
228 elif [[ "$input" == . || "$input" == .. ]]; then
231 [[ $input =~ ^(/?[^/]*)(/?.*)$ ]] || echo NOMATCH >&2
232 output="$output${BASH_REMATCH[1]}"
233 input="${BASH_REMATCH[2]}"
236 printf '%s\n' "${output//\/\//\//}"
239 parse_url() { # eval "$(split_url NAME STRING)" => NAME[...]
242 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
243 [[ $string =~ $re ]] || return $?
245 local scheme="${BASH_REMATCH[2]}"
246 local authority="${BASH_REMATCH[4]}"
247 local path="${BASH_REMATCH[5]}"
248 local query="${BASH_REMATCH[7]}"
249 local fragment="${BASH_REMATCH[9]}"
251 for c in scheme authority query fragment; do
253 printf '%s[%s]=%q\n' "$name" "$c" "${!c}"
255 # unclear if the path is always set even if empty but it looks that way
256 printf '%s[path]=%q\n' "$name" "$path"
259 # is a NAME defined ('set' in bash)?
260 isdefined() { [[ "${!1+x}" ]]; } # isdefined NAME
261 # is a NAME defined AND empty?
262 isempty() { [[ ! "${!1-x}" ]]; } # isempty NAME
269 ssl_cmd=(openssl s_client -crlf -quiet -connect "$server:$port")
270 ssl_cmd+=(-servername "$server") # SNI
271 run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null
275 local url="$1" code meta
277 while read -r -d $'\r' hdr; do
278 code="$(gawk '{print $1}' <<<"$hdr")"
280 gawk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$hdr"
285 log x "[$code] $meta"
291 run prompt "$meta" QUERY
292 run blastoff "?$QUERY"
301 if ((REDIRECTS > BOLLUX_MAXREDIR)); then
302 die $((100 + code)) "Too many redirects!"
309 die "$((100 + code))" "$code"
313 die "$((100 + code))" "$code"
317 die "$((100 + code))" "$code"
319 *) die "$((100 + code)) Unknown response code: $code." ;;
326 mime="$(cut -d\; -f1 <<<"$1" | trim)"
327 charset="$(cut -d\; -f2 <<<"$1" | trim)"
329 *) mime="$(trim <<<"$1")" ;;
332 [[ -z "$mime" ]] && mime="text/gemini"
333 if [[ -z "$charset" ]]; then
336 charset="${charset#charset=}"
339 log debug "mime=$mime; charset=$charset"
345 [[ -r "$BOLLUX_LESSKEY" ]] || mklesskey "$BOLLUX_LESSKEY"
346 } && less_cmd+=(-k "$BOLLUX_LESSKEY")
349 -PM'o\:open, g\:goto, r\:refresh$'
354 if declare -F | grep -q "$submime"; then
355 log d "typeset_$submime"
358 tee "$BOLLUX_PAGESRC" |
359 run "typeset_$submime" |
361 } || run handle_keypress "$?"
366 tee "$BOLLUX_PAGESRC" |
368 } || run handle_keypress "$?"
371 *) run download "$BOLLUX_URL" ;;
376 lesskey -o "$1" - <<-END
378 o quit 0 # 48 open a link
379 g quit 1 # 49 goto a url
381 ] quit 3 # 51 forward
382 r quit 4 # 52 re-request / download
387 gawk 'BEGIN{RS="\n\n"}{gsub(/\r\n?/,"\n");print;print ""}'
394 margin = margin ? margin : 4
418 mark = substr($0, RSTART, RLENGTH)
419 sub(/#+[[:space:]]*/, "", $0)
423 } else if (level == 2) {
431 sub(/=>[[:space:]]*/, "", $0)
434 for (w = 2; w <= NF; w++) {
435 text = text (text ? " " : "") $w
437 fmt = lns "[" (++ln) "]" res " " lts "%s" res "\t" lus "%s" res
441 sub(/\*[[:space:]]*/, "", $0)
445 mark = mark ? mark : mark
446 fmt = fmt ? fmt : "%s"
447 text = text ? text : $0
448 desc = desc ? desc : ""
449 printf ms "%" (margin-1) "s " res fmt "\n", mark, text, desc
450 mark = fmt = text = desc = ""
457 48) # o - open a link -- show a menu of links on the page
458 run select_url "$BOLLUX_PAGESRC"
460 49) # g - goto a url -- input a new url
462 run blastoff -u "$URL"
464 50) # [ - back in the history
467 51) # ] - forward in the history
470 52) # r - re-request the current resource
471 run blastoff "$BOLLUX_URL"
473 *) # 53-57 -- still available for binding
479 run mapfile -t < <(extract_links <"$1")
480 select u in "${MAPFILE[@]}"; do
481 run blastoff "$(gawk '{print $1}' <<<"$u")" && break
488 sub(/=>[[:space:]]*/,"")
490 printf "%s (\033[34m%s\033[0m)\n", $1, $2
498 log x "Downloading: '$BOLLUX_URL' => '$tn'..."
499 dd status=progress >"$tn"
500 fn="$BOLLUX_DOWNDIR/${BOLLUX_URL##*/}"
501 if [[ -f "$fn" ]]; then
503 elif mv "$tn" "$fn"; then
506 log error "Error saving '$fn': downloaded to '$tn'."
510 history_back() { log error "Not implemented."; }
511 history_forward() { log error "Not implemented."; }
513 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
516 BOLLUX_LOGLEVEL=DEBUG