diff options
-rwxr-xr-x | bollux | 343 |
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 |
184 | bollux_init() { | 186 | bollux_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." |
208 | bollux_quit() { | 226 | bollux_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. | ||
214 | trap bollux_quit SIGINT | ||
215 | 230 | ||
216 | # UTILITY FUNCTIONS ############################################################ | 231 | # UTILITY FUNCTIONS ############################################################ |
217 | 232 | ||
@@ -221,11 +236,10 @@ trap bollux_quit SIGINT | |||
221 | run() { # run COMMAND... | 236 | run() { # 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. |
343 | normalize() { | 357 | normalize() { |
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. |
370 | uwellform() { | 384 | uwellform() { # 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. |
394 | usplit() { # usplit NAME:ARRAY URL:STRING | 408 | usplit() { # 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 | } |
424 | ujoin() { # 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. |
457 | ujoin() { # 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. |
454 | ucdef() { # ucdef NAME | 504 | ucdef() { # 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. |
460 | ucblank() { # ucblank NAME | 511 | ucblank() { # 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. |
467 | ucset() { # ucset NAME VALUE | 519 | ucset() { # 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. |
473 | uencode() { # uencode URL:STRING | 536 | uencode() { # 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. |
486 | udecode() { # udecode URL:STRING | 549 | udecode() { # 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". |
492 | pundot() { # pundot PATH:STRING | 555 | pundot() { # 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". |
516 | pmerge() { # pmerge BASE:ARRAY REFERENCE:ARRAY | 579 | pmerge() { # 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. |
613 | gemini_request() { # gemini_request URL | 676 | gemini_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. |
669 | gemini_response() { # gemini_response URL | 735 | gemini_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 | |||
847 | gopher_request() { # gopher_request URL | 913 | gopher_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. |
867 | gopher_response() { # gopher_response URL | 933 | gopher_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 |
1029 | display() { # display METADATA [TITLE] | 1095 | display() { # 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. | ||
1436 | download() { | 1515 | download() { |
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. | ||
1451 | history_append() { # history_append URL TITLE | 1540 | history_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. |
1460 | history_back() { | 1564 | history_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. |
1472 | history_forward() { | 1581 | history_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. | ||
1545 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | 1664 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then |
1546 | ${DEBUG:-false} && set -x | 1665 | ${DEBUG:-false} && set -x |
1547 | run bollux "$@" | 1666 | run bollux "$@" |