about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbollux343
1 files changed, 231 insertions, 112 deletions
diff --git a/bollux b/bollux index d51f444..ebdb22f 100755 --- a/bollux +++ b/bollux
@@ -1,8 +1,9 @@
1#!/usr/bin/env bash 1#!/usr/bin/env bash
2# bollux: a bash gemini client 2################################################################################
3# BOLLUX: a bash gemini client
3# Author: Case Duckworth 4# Author: Case Duckworth
4# License: MIT 5# License: MIT
5# Version: 0.4.0 6# Version: 0.4.1
6# 7#
7# Commentary: 8# Commentary:
8# 9#
@@ -46,6 +47,7 @@
46# [9]: OpenSSL `s_client' online manual 47# [9]: OpenSSL `s_client' online manual
47# https://www.openssl.org/docs/manmaster/man1/openssl-s_client.html 48# https://www.openssl.org/docs/manmaster/man1/openssl-s_client.html
48# 49#
50################################################################################
49# Code: 51# Code:
50 52
51# Program information 53# Program information
@@ -182,15 +184,31 @@ bollux_config() {
182 184
183# Initialize bollux state 185# Initialize bollux state
184bollux_init() { 186bollux_init() {
185 # Trap cleanup 187 # Trap `bollux_cleanup' on quit and exit
186 trap bollux_cleanup INT QUIT EXIT 188 trap bollux_cleanup INT QUIT EXIT
187 # State 189 # Trap `bollux_quit' on interrupt (C-c)
188 REDIRECTS=0 190 trap bollux_quit SIGINT
191
192 # Disable pathname expansion.
193 #
194 # It's very unlikely the user will want to navigate to a file when
195 # answering the GO prompt.
189 set -f 196 set -f
197
198 # Initialize state
199 #
200 # Other than $REDIRECTS, bollux's mutable state includes
201 # $BOLLUX_URL, but that's initialized elsewhere (possibly even by
202 # the user)
203 REDIRECTS=0
204
190 # History 205 # History
206 #
207 # See also `history_append', `history_back', `history_forward'
191 declare -a HISTORY # history is kept in an array 208 declare -a HISTORY # history is kept in an array
192 HN=0 # position of history in the array 209 HN=0 # position of history in the array
193 run mkdir -p "${BOLLUX_HISTFILE%/*}" 210 run mkdir -p "${BOLLUX_HISTFILE%/*}"
211
194 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds) 212 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds)
195 run rm -f "$BOLLUX_LESSKEY" 213 run rm -f "$BOLLUX_LESSKEY"
196 mklesskey 214 mklesskey
@@ -206,12 +224,9 @@ bollux_cleanup() {
206# 224#
207# The default message is from the wonderful show "Cowboy Bebop." 225# The default message is from the wonderful show "Cowboy Bebop."
208bollux_quit() { 226bollux_quit() {
209 bollux_cleanup
210 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG" 227 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
211 exit 228 exit
212} 229}
213# SIGINT is C-c, and I want to make sure bollux quits when it's typed.
214trap bollux_quit SIGINT
215 230
216# UTILITY FUNCTIONS ############################################################ 231# UTILITY FUNCTIONS ############################################################
217 232
@@ -221,11 +236,10 @@ trap bollux_quit SIGINT
221run() { # run COMMAND... 236run() { # run COMMAND...
222 # I have to add a `trap' here for SIGINT to work properly. 237 # I have to add a `trap' here for SIGINT to work properly.
223 trap bollux_quit SIGINT 238 trap bollux_quit SIGINT
224 log debug "$*" 239 LOG_FUNC=2 log debug "> $*"
225 "$@" 240 "$@"
226} 241}
227 242
228
229# Log a message to stderr (&2). 243# Log a message to stderr (&2).
230# 244#
231# `log' in this script can take 3 different parameters: `d', `e', and `x', where 245# `log' in this script can take 3 different parameters: `d', `e', and `x', where
@@ -254,8 +268,8 @@ log() { # log LEVEL MESSAGE...
254 esac 268 esac
255 shift 269 shift
256 270
257 printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' \ 271 printf >&2 '\e[%sm%s:%-16s:\e[0m %s\n' \
258 "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*" 272 "$fmt" "$PRGN" "${FUNCNAME[${LOG_FUNC:-1}]}" "$*"
259} 273}
260 274
261# Exit with an error and a message describing it. 275# Exit with an error and a message describing it.
@@ -341,12 +355,12 @@ sleep() { # sleep SECONDS
341 355
342# Normalize files. 356# Normalize files.
343normalize() { 357normalize() {
344 shopt -s extglob 358 shopt -s extglob # for the printf call below
345 while IFS= read -r; do 359 while IFS= read -r; do
346 # Normalize line endings to Unix-style (LF) 360 # Normalize line endings to Unix-style (LF)
347 printf '%s\n' "${REPLY//$'\r'?($'\n')/}" 361 printf '%s\n' "${REPLY//$'\r'?($'\n')/}"
348 done 362 done
349 shopt -u extglob 363 shopt -u extglob # reset 'extglob'
350} 364}
351 365
352# URLS ######################################################################### 366# URLS #########################################################################
@@ -367,16 +381,16 @@ normalize() {
367# trim whitespace. 381# trim whitespace.
368# 382#
369# Useful for URLs that were probably input by humans. 383# Useful for URLs that were probably input by humans.
370uwellform() { 384uwellform() { # uwellform URL
371 local u="$1" 385 local url="$1"
372 386
373 if [[ "$u" != *://* ]]; then 387 if [[ "$url" != *://* ]]; then
374 u="$BOLLUX_PROTO://$u" 388 url="$BOLLUX_PROTO://$url"
375 fi 389 fi
376 390
377 u="$(trim_string "$u")" 391 url="$(trim_string "$url")"
378 392
379 printf '%s\n' "$u" 393 printf '%s\n' "$url"
380} 394}
381 395
382# Split a URL into its constituent parts, placing them all in the given array. 396# Split a URL into its constituent parts, placing them all in the given array.
@@ -391,58 +405,94 @@ uwellform() {
391# takes the matched URL, splits it using the regex, then assigns each part to an 405# takes the matched URL, splits it using the regex, then assigns each part to an
392# element of the url array NAME by using `printf -v', which prints to a 406# element of the url array NAME by using `printf -v', which prints to a
393# variable. 407# variable.
394usplit() { # usplit NAME:ARRAY URL:STRING 408usplit() { # usplit URL_ARRAY<name> URL
409 # Note: URL_ARRAY isn't assigned in `usplit', because it should
410 # already exist. Pass /only/ the name of URL_ARRAY to this
411 # function, not its contents.
395 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' 412 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
396 [[ $2 =~ $re ]] || return $? 413 local u="$2"
414 [[ "$u" =~ $re ]] || {
415 exit_code=$?
416 log error "usplit: '$2' doesn't match '$re'"
417 return $?
418 }
397 419
398 # ShellCheck doesn't see that I'm using these variables in the `for' 420 # ShellCheck doesn't see that I'm using these variables in the `for'
399 # loop below, because I'm not technically using them /as/ variables, but 421 # loop below, because I'm not technically using them /as/ variables, but
400 # as names to the variables. The ${!c} formation in the `printf' call 422 # as names to the variables. The ${!c} formation in the `printf' call
401 # below performs a reverse lookup on the name to get the actual data. 423 # below performs a reverse lookup on the name to get the actual data.
402 # shellcheck disable=2034 424 # shellcheck disable=2034
403 local url="${BASH_REMATCH[0]}" \ 425 local entire_url="${BASH_REMATCH[0]}" \
404 scheme="${BASH_REMATCH[2]}" \ 426 scheme="${BASH_REMATCH[2]}" \
405 authority="${BASH_REMATCH[4]}" \ 427 authority="${BASH_REMATCH[4]}" \
406 path="${BASH_REMATCH[5]}" \ 428 path="${BASH_REMATCH[5]}" \
407 query="${BASH_REMATCH[7]}" \ 429 query="${BASH_REMATCH[7]}" \
408 fragment="${BASH_REMATCH[9]}" 430 fragment="${BASH_REMATCH[9]}"
409 431
432 # Iterate through the 5 components of a URL and assign them to elements
433 # of URL_ARRAY, as follows:
410 # 0=url 1=scheme 2=authority 3=path 4=query 5=fragment 434 # 0=url 1=scheme 2=authority 3=path 4=query 5=fragment
411 local i=1 c 435 run printf -v "$1[0]" '%s' "$entire_url"
436 # This loop tests whether the component exists first -- if it
437 # doesn't, the special variable $UC_BLANK is used in the spot
438 # instead. Bash doesn't have a useful way of differentiating an
439 # /unset/ element of an array, versus an /empty/ element.
440 # The only exception is that 'path' component, which always exists
441 # in a URL (I think the simplest URL possible is '/', the empty
442 # path).
443 local i=1 # begin at 1 -- the full URL is [0].
412 for c in scheme authority path query fragment; do 444 for c in scheme authority path query fragment; do
413 if [[ "${!c}" || "$c" == path ]]; then 445 if [[ "${!c}" || "$c" == path ]]; then
414 printf -v "$1[$i]" '%s' "${!c}" 446 run printf -v "$1[$i]" '%s' "${!c}"
415 else 447 else
416 printf -v "$1[$i]" '%s' "$UC_BLANK" 448 run printf -v "$1[$i]" '%s' "$UC_BLANK"
417 fi 449 fi
418 ((i += 1)) 450 ((i += 1))
419 done 451 done
420 printf -v "$1[0]" '%s' "$url"
421}
422 452
423# Join a URL array (NAME) back into a string. 453}
424ujoin() { # ujoin NAME:ARRAY
425 local -n U="$1"
426 454
427 if ucdef U[1]; then 455# Join a URL array, split with `usplit', back into a string, assigning
428 printf -v U[0] "%s:" "${U[1]}" 456# it to the 0th element of the array.
457ujoin() { # ujoin URL_ARRAY<name>
458 # Here's the documentation for the '-n' flag:
459 #
460 # Give each name the nameref attribute, making it a name reference
461 # to another variable. That other variable is defined by the value of
462 # name. All references, assignments, and attribute modifications to
463 # name, except for those using or changing the -n attribute itself,
464 # are performed on the variable referenced by name's value. The
465 # nameref attribute cannot be applied to array variables.
466 #
467 # Pretty handy for passing-by-name! Except that last part -- "The
468 # nameref attribute cannot be applied to array variables." However,
469 # I've found a clever hack -- you can use 'printf -v' to print the
470 # value to the array element.
471 local -n URL_ARRAY="$1"
472
473 # For each possible URL component, check if it exists with `ucdef'.
474 # If it does, append it (with the correct component delimiter) to
475 # URL_ARRAY[0].
476 if ucdef URL_ARRAY[1]; then
477 printf -v URL_ARRAY[0] "%s:" "${URL_ARRAY[1]}"
429 fi 478 fi
430 479
431 if ucdef U[2]; then 480 if ucdef URL_ARRAY[2]; then
432 printf -v U[0] "${U[0]}//%s" "${U[2]}" 481 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}//%s" "${URL_ARRAY[2]}"
433 fi 482 fi
434 483
435 printf -v U[0] "${U[0]}%s" "${U[3]}" 484 # The path component is required.
485 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}%s" "${URL_ARRAY[3]}"
436 486
437 if ucdef U[4]; then 487 if ucdef URL_ARRAY[4]; then
438 printf -v U[0] "${U[0]}?%s" "${U[4]}" 488 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}?%s" "${URL_ARRAY[4]}"
439 fi 489 fi
440 490
441 if ucdef U[5]; then 491 if ucdef URL_ARRAY[5]; then
442 printf -v U[0] "${U[0]}#%s" "${U[5]}" 492 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}#%s" "${URL_ARRAY[5]}"
443 fi 493 fi
444 494
445 log d "${U[0]}" 495 log d "${URL_ARRAY[0]}"
446} 496}
447 497
448# `ucdef' checks whether a URL component is blank or not -- if a component 498# `ucdef' checks whether a URL component is blank or not -- if a component
@@ -451,26 +501,39 @@ ujoin() { # ujoin NAME:ARRAY
451# not going to really be in a URL). I tried really hard to differentiate an 501# not going to really be in a URL). I tried really hard to differentiate an
452# unset array element from a simply empty one, but like, as far as I could tell, 502# unset array element from a simply empty one, but like, as far as I could tell,
453# you can't do that in Bash. 503# you can't do that in Bash.
454ucdef() { # ucdef NAME 504ucdef() { # ucdef COMPONENT<name>
455 [[ "${!1}" != "$UC_BLANK" ]] 505 local component="$1"
506 [[ "${!component}" != "$UC_BLANK" ]]
456} 507}
457 508
458# `ucblank' determines whether a URL component is blank (""), as opposed to 509# `ucblank' determines whether a URL component is blank (""), as opposed to
459# undefined. 510# undefined.
460ucblank() { # ucblank NAME 511ucblank() { # ucblank COMPONENT<name>
461 [[ -z "${!1}" ]] 512 local component="$1"
513 [[ -z "${!component}" ]]
462} 514}
463 515
464# `ucset' sets one component of a URL array and setting the 0th element to the 516# `ucset' sets one component of a URL array and setting the 0th element to the
465# new full URL. Use it instead of directly setting the array element with U[x], 517# new full URL. Use it instead of directly setting the array element with U[x],
466# because U[0] will fall out of sync with the rest of the contents. 518# because U[0] will fall out of sync with the rest of the contents.
467ucset() { # ucset NAME VALUE 519ucset() { # ucset URL_ARRAY_INDEX<name> NEW_VALUE
468 run eval "${1}='$2'" 520 local url_array_component="$1" # Of form 'URL_ARRAY[INDEX]'
469 run ujoin "${1/\[*\]/}" 521 local value="$2"
522
523 # Assign $value to $url_array_component.
524 #
525 # Wrapped in an 'eval' for the extra layer of indirection.
526 run eval "${url_array_component}='$value'"
527
528 # Rejoin the URL_ARRAY with the changed value.
529 #
530 # The substitution here strips the array index subscript (i.e.,
531 # URL[4] => URL), passing the name of the full array to `ujoin'.
532 run ujoin "${url_array_component/\[*\]/}"
470} 533}
471 534
472# [1]: encode a URL using percent-encoding. 535# [1]: Encode a URL using percent-encoding.
473uencode() { # uencode URL:STRING 536uencode() { # uencode URL
474 local LC_ALL=C 537 local LC_ALL=C
475 for ((i = 0; i < ${#1}; i++)); do 538 for ((i = 0; i < ${#1}; i++)); do
476 : "${1:i:1}" 539 : "${1:i:1}"
@@ -482,14 +545,14 @@ uencode() { # uencode URL:STRING
482 printf '\n' 545 printf '\n'
483} 546}
484 547
485# [1]: decode a percent-encoded URL. 548# [1]: Decode a percent-encoded URL.
486udecode() { # udecode URL:STRING 549udecode() { # udecode URL
487 : "${1//+/ }" 550 : "${1//+/ }"
488 printf '%b\n' "${_//%/\\x}" 551 printf '%b\n' "${_//%/\\x}"
489} 552}
490 553
491# Implement [2] § 5.2.4, "Remove Dot Segments" 554# Implement [2]: 5.2.4, "Remove Dot Segments".
492pundot() { # pundot PATH:STRING 555pundot() { # pundot PATH
493 local input="$1" 556 local input="$1"
494 local output 557 local output
495 while [[ "$input" ]]; do 558 while [[ "$input" ]]; do
@@ -512,28 +575,28 @@ pundot() { # pundot PATH:STRING
512 printf '%s\n' "${output//\/\//\//}" 575 printf '%s\n' "${output//\/\//\//}"
513} 576}
514 577
515# Implement [2] § 5.2.3, "Merge Paths" 578# Implement [2] Section 5.2.3, "Merge Paths".
516pmerge() { # pmerge BASE:ARRAY REFERENCE:ARRAY 579pmerge() { # pmerge BASE_PATH<name> REFERENCE_PATH<name>
517 local -n b="$1" 580 local -n base_path="$1"
518 local -n r="$2" 581 local -n reference_path="$2"
519 582
520 if ucblank r[3]; then 583 if ucblank reference_path[3]; then
521 printf '%s\n' "${b[3]//\/\//\//}" 584 printf '%s\n' "${base_path[3]//\/\//\//}"
522 return 585 return
523 fi 586 fi
524 587
525 if ucdef b[2] && ucblank b[3]; then 588 if ucdef base_path[2] && ucblank base_path[3]; then
526 printf '/%s\n' "${r[3]//\/\//\//}" 589 printf '/%s\n' "${reference_path[3]//\/\//\//}"
527 else 590 else
528 local bp="" 591 local bp=""
529 if [[ "${b[3]}" == */* ]]; then 592 if [[ "${base_path[3]}" == */* ]]; then
530 bp="${b[3]%/*}" 593 bp="${base_path[3]%/*}"
531 fi 594 fi
532 printf '%s/%s\n' "${bp%/}" "${r[3]#/}" 595 printf '%s/%s\n' "${bp%/}" "${reference_path[3]#/}"
533 fi 596 fi
534} 597}
535 598
536# `utransform' implements [2]6 § 5.2.2, "Transform Resources." 599# `utransform' implements [2]6 Section 5.2.2, "Transform Resources."
537# 600#
538# That section conveniently lays out a pseudocode algorithm describing how URL 601# That section conveniently lays out a pseudocode algorithm describing how URL
539# resources should be transformed from one to another. This function just 602# resources should be transformed from one to another. This function just
@@ -609,19 +672,21 @@ utransform() { # utransform TARGET:ARRAY BASE:STRING REFERENCE:STRING
609# 672#
610################################################################################ 673################################################################################
611 674
612# Request a resource from a gemini server - see [3] §§ 2, 4. 675# Request a resource from a gemini server - see [3] Sections 2, 4.
613gemini_request() { # gemini_request URL 676gemini_request() { # gemini_request URL
614 local -a url 677 local -a url
615 usplit url "$1" 678 run usplit url "$1"
679 log debug "${url[@]}"
616 680
617 # Remove user info from the URL. 681 # Remove user info from the URL.
618 # 682 #
619 # URLs can technically be of the form <proto>://<user>:<pass>@<domain> 683 # URLs can technically be of the form <proto>://<user>:<pass>@<domain>
620 # (see [2], § 3.2, "Authority"). I don't know of any Gemini servers 684 # (see [2] Section 3.2, "Authority"). I don't know of any Gemini servers
621 # that use the <user> or <pass> parts, so `gemini_request' just strips 685 # that use the <user> or <pass> parts, so `gemini_request' just strips
622 # them from the requested URL. This will need to be changed if servers 686 # them from the requested URL. This will need to be changed if servers
623 # decide to use this method of authentication. 687 # decide to use this method of authentication.
624 ucset url[2] "${url[2]#*@}" 688 log debug "Removing user info from the URL"
689 run ucset url[2] "${url[2]#*@}"
625 690
626 # Determine the port to request. 691 # Determine the port to request.
627 # 692 #
@@ -630,6 +695,7 @@ gemini_request() { # gemini_request URL
630 # port can be specified after the domain, separated with a colon. The 695 # port can be specified after the domain, separated with a colon. The
631 # user can also request a different default port, for whatever reason, 696 # user can also request a different default port, for whatever reason,
632 # by setting the variable $BOLLUX_GEMINI_PORT. 697 # by setting the variable $BOLLUX_GEMINI_PORT.
698 log debug "Determining the port to request"
633 local port 699 local port
634 if [[ "${url[2]}" == *:* ]]; then 700 if [[ "${url[2]}" == *:* ]]; then
635 port="${url[2]#*:}" 701 port="${url[2]#*:}"
@@ -665,7 +731,7 @@ gemini_request() { # gemini_request URL
665 run "${ssl_cmd[@]}" <<<"$url" 731 run "${ssl_cmd[@]}" <<<"$url"
666} 732}
667 733
668# Handle the gemini response - see [3] § 3. 734# Handle the gemini response - see [3] Section 3.
669gemini_response() { # gemini_response URL 735gemini_response() { # gemini_response URL
670 local code meta # received on the first line of the response 736 local code meta # received on the first line of the response
671 local title # determined by a clunky heuristic, see read loop: (2*) 737 local title # determined by a clunky heuristic, see read loop: (2*)
@@ -685,12 +751,12 @@ gemini_response() { # gemini_response URL
685 # `download', below), but I'm not sure how to remedy that issue either. 751 # `download', below), but I'm not sure how to remedy that issue either.
686 # It requires more research. 752 # It requires more research.
687 while read -t "$BOLLUX_TIMEOUT" -r code meta || 753 while read -t "$BOLLUX_TIMEOUT" -r code meta ||
688 { (($? > 128)) && die 99 "Timeout."; }; do 754 { (($? > 128)) && die 99 "Timeout."; }; do
689 break 755 break
690 done 756 done
691 log d "[$code] $meta" 757 log d "[$code] $meta"
692 758
693 # Branch depending on the status code. See [3], Appendix 1. 759 # Branch depending on the status code. See [3] Appendix 1.
694 # 760 #
695 # Notes: 761 # Notes:
696 # - All codes other than 3* (Redirects) reset the REDIRECTS counter. 762 # - All codes other than 3* (Redirects) reset the REDIRECTS counter.
@@ -720,7 +786,7 @@ gemini_response() { # gemini_response URL
720 # 786 #
721 # This while loop reads through the file looking for a line 787 # This while loop reads through the file looking for a line
722 # starting with `#', which is a level-one heading in text/gemini 788 # starting with `#', which is a level-one heading in text/gemini
723 # (see [3], § 5). It assumes that the first such heading is the 789 # (see [3] Section 5). It assumes that the first such heading is the
724 # title of the page, and uses that title for the terminal title 790 # title of the page, and uses that title for the terminal title
725 # and for the history. 791 # and for the history.
726 local pretitle 792 local pretitle
@@ -756,7 +822,7 @@ gemini_response() { # gemini_response URL
756 # distinction. I'm not sure what the difference would be in 822 # distinction. I'm not sure what the difference would be in
757 # practice, anyway. 823 # practice, anyway.
758 # 824 #
759 # Per [4], bollux limits the number of redirects a page is 825 # Per [4] bollux limits the number of redirects a page is
760 # allowed to make (by default, five). Change `$BOLLUX_MAXREDIR' 826 # allowed to make (by default, five). Change `$BOLLUX_MAXREDIR'
761 # to customize that limit. 827 # to customize that limit.
762 ((REDIRECTS += 1)) 828 ((REDIRECTS += 1))
@@ -773,7 +839,7 @@ gemini_response() { # gemini_response URL
773 run blastoff "$meta" # TODO: confirm redirect 839 run blastoff "$meta" # TODO: confirm redirect
774 ;; 840 ;;
775 (4*) # TEMPORARY ERROR 841 (4*) # TEMPORARY ERROR
776 # Since the 4* codes ([3], Appendix 1) are all server issues, 842 # Since the 4* codes ([3] Appendix 1) are all server issues,
777 # bollux can treat them all basically the same. This is an area 843 # bollux can treat them all basically the same. This is an area
778 # that could use some expansion. 844 # that could use some expansion.
779 local desc="Temporary error" 845 local desc="Temporary error"
@@ -847,7 +913,7 @@ gemini_response() { # gemini_response URL
847gopher_request() { # gopher_request URL 913gopher_request() { # gopher_request URL
848 local url="$1" 914 local url="$1"
849 915
850 # [7] § 2.1 916 # [7] Section 2.1
851 [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] 917 [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]]
852 local server="${BASH_REMATCH[1]}" \ 918 local server="${BASH_REMATCH[1]}" \
853 port="${BASH_REMATCH[3]:-$BOLLUX_GOPHER_PORT}" \ 919 port="${BASH_REMATCH[3]:-$BOLLUX_GOPHER_PORT}" \
@@ -866,7 +932,7 @@ gopher_request() { # gopher_request URL
866# Handle a server response. 932# Handle a server response.
867gopher_response() { # gopher_response URL 933gopher_response() { # gopher_response URL
868 local url="$1" pre=false 934 local url="$1" pre=false
869 # [7] § 2.1 935 # [7] Section 2.1
870 # 936 #
871 # Note that this duplicates the code in `gopher_request'. There might 937 # Note that this duplicates the code in `gopher_request'. There might
872 # be a good way to thread this data through so that it's not computed 938 # be a good way to thread this data through so that it's not computed
@@ -881,7 +947,7 @@ gopher_response() { # gopher_response URL
881 # basically, each line in a gophermap starts with a character, its type, 947 # basically, each line in a gophermap starts with a character, its type,
882 # and then is followed by a series of tab-separated fields describing 948 # and then is followed by a series of tab-separated fields describing
883 # where that type is and how to display it. The full list of original 949 # where that type is and how to display it. The full list of original
884 # line types can be found in [6] § 3.8, though the types have also been 950 # line types can be found in [6] Section 3.8, though the types have also been
885 # extended over the years. Since bollux can only display types that are 951 # extended over the years. Since bollux can only display types that are
886 # text-ish, it only concerns itself with those in this case statement. 952 # text-ish, it only concerns itself with those in this case statement.
887 # All the others are simply downloaded. 953 # All the others are simply downloaded.
@@ -915,7 +981,7 @@ gopher_response() { # gopher_response URL
915 fi 981 fi
916 ;; 982 ;;
917 (*) # Anything else 983 (*) # Anything else
918 # The list at [6] § 3.8 includes the following (noted where it 984 # The list at [6] Section 3.8 includes the following (noted where it
919 # might be good to differently handle them in the future): 985 # might be good to differently handle them in the future):
920 # 986 #
921 # 2. Item is a CSO phone-book server ***** 987 # 2. Item is a CSO phone-book server *****
@@ -940,7 +1006,7 @@ gopher_response() { # gopher_response URL
940 1006
941# Convert a gophermap naively to a gemini page. 1007# Convert a gophermap naively to a gemini page.
942# 1008#
943# Based strongly on [8], but bash-ified. Due to the properties of link lines in 1009# Based strongly on [8] but bash-ified. Due to the properties of link lines in
944# gemini, many of the item types in `gemini_reponse' can be linked to the proper 1010# gemini, many of the item types in `gemini_reponse' can be linked to the proper
945# protocol handlers here -- so if a user is trying to reach a TCP link through 1011# protocol handlers here -- so if a user is trying to reach a TCP link through
946# gopher, bollux won't have to handle it, for example.* 1012# gopher, bollux won't have to handle it, for example.*
@@ -998,7 +1064,7 @@ gopher_convert() {
998 pre=false 1064 pre=false
999 fi 1065 fi
1000 printf '=> telnet://%s:%s/%s%s %s\n' \ 1066 printf '=> telnet://%s:%s/%s%s %s\n' \
1001 "$server" "$port" "$type" "$path" "$label" 1067 "$server" "$port" "$type" "$path" "$label"
1002 ;; 1068 ;;
1003 (*) # other type 1069 (*) # other type
1004 if $pre; then 1070 if $pre; then
@@ -1006,7 +1072,7 @@ gopher_convert() {
1006 pre=false 1072 pre=false
1007 fi 1073 fi
1008 printf '=> gopher://%s:%s/%s%s %s\n' \ 1074 printf '=> gopher://%s:%s/%s%s %s\n' \
1009 "$server" "$port" "$type" "$path" "$label" 1075 "$server" "$port" "$type" "$path" "$label"
1010 ;; 1076 ;;
1011 esac 1077 esac
1012 done 1078 done
@@ -1028,7 +1094,8 @@ gopher_convert() {
1028# display the fetched content 1094# display the fetched content
1029display() { # display METADATA [TITLE] 1095display() { # display METADATA [TITLE]
1030 local -a less_cmd 1096 local -a less_cmd
1031 local i mime charset 1097 local mime charset
1098
1032 # split header line 1099 # split header line
1033 local -a hdr 1100 local -a hdr
1034 IFS=';' read -ra hdr <<<"$1" 1101 IFS=';' read -ra hdr <<<"$1"
@@ -1178,7 +1245,7 @@ typeset_gemini() {
1178 ;; 1245 ;;
1179 (alt | both) 1246 (alt | both)
1180 $pre && PRE_LINE_FORCE=true \ 1247 $pre && PRE_LINE_FORCE=true \
1181 gemini_pre "${REPLY#\`\`\`}" 1248 gemini_pre "${REPLY#\`\`\`}"
1182 ;; 1249 ;;
1183 esac 1250 esac
1184 continue 1251 continue
@@ -1215,13 +1282,13 @@ gemini_link() {
1215 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1282 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1216 printf "\e[${C_LINK_NUMBER}m[%d]${C_RESET} " "$ln" 1283 printf "\e[${C_LINK_NUMBER}m[%d]${C_RESET} " "$ln"
1217 fold_line -n -B "\e[${C_LINK_TITLE}m" -A "${C_RESET}" \ 1284 fold_line -n -B "\e[${C_LINK_TITLE}m" -A "${C_RESET}" \
1218 -l "$((${#ln} + 3))" -m "${T_MARGIN}" \ 1285 -l "$((${#ln} + 3))" -m "${T_MARGIN}" \
1219 "$WIDTH" "$(trim_string "$t")" 1286 "$WIDTH" "$(trim_string "$t")"
1220 fold_line -B " \e[${C_LINK_URL}m" \ 1287 fold_line -B " \e[${C_LINK_URL}m" \
1221 -A "${C_RESET}" \ 1288 -A "${C_RESET}" \
1222 -l "$((${#ln} + 3 + ${#t}))" \ 1289 -l "$((${#ln} + 3 + ${#t}))" \
1223 -m "$((T_MARGIN + ${#ln} + 2))" \ 1290 -m "$((T_MARGIN + ${#ln} + 2))" \
1224 "$WIDTH" "$a" 1291 "$WIDTH" "$a"
1225 else 1292 else
1226 gemini_pre "$1" 1293 gemini_pre "$1"
1227 fi 1294 fi
@@ -1239,7 +1306,7 @@ gemini_header() {
1239 1306
1240 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1307 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1241 fold_line -B "\e[${hdrfmt}m" -A "${C_RESET}" -m "${T_MARGIN}" \ 1308 fold_line -B "\e[${hdrfmt}m" -A "${C_RESET}" -m "${T_MARGIN}" \
1242 "$WIDTH" "$t" 1309 "$WIDTH" "$t"
1243 else 1310 else
1244 gemini_pre "$1" 1311 gemini_pre "$1"
1245 fi 1312 fi
@@ -1254,7 +1321,7 @@ gemini_list() {
1254 1321
1255 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1322 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1256 fold_line -B "\e[${C_LIST}m" -A "${C_RESET}" -m "$T_MARGIN" \ 1323 fold_line -B "\e[${C_LIST}m" -A "${C_RESET}" -m "$T_MARGIN" \
1257 "$WIDTH" "$t" 1324 "$WIDTH" "$t"
1258 else 1325 else
1259 gemini_pre "$1" 1326 gemini_pre "$1"
1260 fi 1327 fi
@@ -1269,7 +1336,7 @@ gemini_quote() {
1269 1336
1270 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1337 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1271 fold_line -B "\e[${C_QUOTE}m" -A "${C_RESET}" -m "$T_MARGIN" \ 1338 fold_line -B "\e[${C_QUOTE}m" -A "${C_RESET}" -m "$T_MARGIN" \
1272 "$WIDTH" "$t" 1339 "$WIDTH" "$t"
1273 else 1340 else
1274 gemini_pre "$1" 1341 gemini_pre "$1"
1275 fi 1342 fi
@@ -1279,7 +1346,7 @@ gemini_text() {
1279 if ! ${2-false}; then 1346 if ! ${2-false}; then
1280 printf "%${S_MARGIN}s " ' ' 1347 printf "%${S_MARGIN}s " ' '
1281 fold_line -m "$T_MARGIN" \ 1348 fold_line -m "$T_MARGIN" \
1282 "$WIDTH" "$1" 1349 "$WIDTH" "$1"
1283 else 1350 else
1284 gemini_pre "$1" 1351 gemini_pre "$1"
1285 fi 1352 fi
@@ -1432,7 +1499,19 @@ extract_links() {
1432 done 1499 done
1433} 1500}
1434 1501
1435# download $BOLLUX_URL 1502# Download a file.
1503#
1504# Any non-otherwise-handled MIME type will be downloaded using this function.
1505# It uses 'dd' to download the resource to a temporary file, then attempts to
1506# move it to $BOLLUX_DOWNDIR (by default, $PWD). If that's not possible (either
1507# because the target file already exists or the 'mv' invocation fails for some
1508# reason), `download' logs the error and alerts the user where the temporary
1509# file is saved.
1510#
1511# `download' works by reading the end of the pipe from `display', which means
1512# that sometimes, due to something with the way bash or while or ... something
1513# ... chunks the data, sometimes binary data gets corrupted. This is an area
1514# that requires more research.
1436download() { 1515download() {
1437 tn="$(mktemp)" 1516 tn="$(mktemp)"
1438 log x "Downloading: '$BOLLUX_URL' => '$tn'..." 1517 log x "Downloading: '$BOLLUX_URL' => '$tn'..."
@@ -1447,35 +1526,67 @@ download() {
1447 fi 1526 fi
1448} 1527}
1449 1528
1450# append a URL to history 1529# HISTORY #####################################################################
1530#
1531# While bollux saves history to a file ($BOLLUX_HISTFILE), it doesn't /do/
1532# anything with the history that's been saved. When I do implement the history
1533# functionality, it'll probably be on top of a file:// protocol, which will make
1534# it very simple to also implement bookmarks and the previewing of pages. In
1535# fact, I should be able to implement this change by the weekend (2021-03-07).
1536#
1537###############################################################################
1538
1539# Append a URL to history.
1451history_append() { # history_append URL TITLE 1540history_append() { # history_append URL TITLE
1452 BOLLUX_URL="$1" 1541 local url="$1"
1453 # date/time, url, title (best guess) 1542 local title="$2"
1454 run printf '%(%FT%T)T\t%s\t%s\n' -1 "$1" "$2" >>"$BOLLUX_HISTFILE" 1543
1455 HISTORY[$HN]="$BOLLUX_URL" 1544 # Print the URL and its title (if given) to $BOLLUX_HISTFILE.
1545 local fmt=''
1546 fmt+='%(%FT%T)T\t' # %(_)T calls directly to 'strftime'.
1547 if (( $# == 2 )); then
1548 fmt+='%s\t' # $url
1549 fmt+='%s\n' # $title
1550 else
1551 fmt+='%s%s\n' # printf needs a field for every argument.
1552 fi
1553 run printf -- "$fmt" -1 "$url" "$title" >>"$BOLLUX_HISTFILE"
1554
1555 # Add the URL to the HISTORY array and increment the pointer.
1556 HISTORY[$HN]="$url"
1456 ((HN += 1)) 1557 ((HN += 1))
1558
1559 # Update $BOLLUX_URL.
1560 BOLLUX_URL="$url"
1457} 1561}
1458 1562
1459# move back in history (session) 1563# Move back in session history.
1460history_back() { 1564history_back() {
1461 log d "HN=$HN" 1565 log d "HN=$HN"
1566 # We need to subtract 2 from HN because it automatically increases by
1567 # one with each call to `history_append'. If we subtract 1, we'll just
1568 # be at the end of the array again, reloading the page.
1462 ((HN -= 2)) 1569 ((HN -= 2))
1570
1463 if ((HN < 0)); then 1571 if ((HN < 0)); then
1464 HN=0 1572 HN=0
1465 log e "Beginning of history." 1573 log e "Beginning of history."
1466 return 1 1574 return 1
1467 fi 1575 fi
1576
1468 run blastoff "${HISTORY[$HN]}" 1577 run blastoff "${HISTORY[$HN]}"
1469} 1578}
1470 1579
1471# move forward in history (session) 1580# Move forward in session history.
1472history_forward() { 1581history_forward() {
1473 log d "HN=$HN" 1582 log d "HN=$HN"
1583
1474 if ((HN >= ${#HISTORY[@]})); then 1584 if ((HN >= ${#HISTORY[@]})); then
1475 HN="${#HISTORY[@]}" 1585 HN="${#HISTORY[@]}"
1476 log e "End of history." 1586 log e "End of history."
1477 return 1 1587 return 1
1478 fi 1588 fi
1589
1479 run blastoff "${HISTORY[$HN]}" 1590 run blastoff "${HISTORY[$HN]}"
1480} 1591}
1481 1592
@@ -1499,7 +1610,7 @@ blastoff() { # blastoff [-u] URL
1499 fi 1610 fi
1500 1611
1501 # After ensuring the URL is well-formed, `blastoff' needs to transform 1612 # After ensuring the URL is well-formed, `blastoff' needs to transform
1502 # it according to the transform rules of RFC 3986 (see §5.2.2), which 1613 # it according to the transform rules of RFC 3986 (see Section 5.2.2), which
1503 # turns relative references into absolute references that bollux can use 1614 # turns relative references into absolute references that bollux can use
1504 # in its request to the server. That's followed by a check that the 1615 # in its request to the server. That's followed by a check that the
1505 # protocol is set, defaulting to Gemini if it isn't. 1616 # protocol is set, defaulting to Gemini if it isn't.
@@ -1535,13 +1646,21 @@ blastoff() { # blastoff [-u] URL
1535 run "${url[1]}_response" "$url" 1646 run "${url[1]}_response" "$url"
1536 else 1647 else
1537 log d \ 1648 log d \
1538 "No response handler for '${url[1]}';" \ 1649 "No response handler for '${url[1]}';" \
1539 " passing thru" 1650 " passing thru"
1540 passthru 1651 passthru
1541 fi 1652 fi
1542 } 1653 }
1543} 1654}
1544 1655
1656# $BASH_SOURCE is an array that stores the "stack" of source calls in bash. If
1657# the first element of that array is "bollux", that means the user called this
1658# script, instead of sourcing it. In that case, and ONLY in that case, should
1659# bollux actually enter the main loop of the program. Otherwise, allow the
1660# sourcing environment to simply source this script.
1661#
1662# This is basically the equivalent of python's 'if __name__ == "__main__":'
1663# block.
1545if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then 1664if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
1546 ${DEBUG:-false} && set -x 1665 ${DEBUG:-false} && set -x
1547 run bollux "$@" 1666 run bollux "$@"