diff options
-rwxr-xr-x | bollux | 432 |
1 files changed, 270 insertions, 162 deletions
diff --git a/bollux b/bollux index a4e9584..46c381b 100755 --- a/bollux +++ b/bollux | |||
@@ -23,17 +23,28 @@ | |||
23 | # things. That's a major project though, and I'm scared. | 23 | # things. That's a major project though, and I'm scared. |
24 | # | 24 | # |
25 | # The following works were referenced when writing this, and I've tried to | 25 | # The following works were referenced when writing this, and I've tried to |
26 | # credit them in comments below. Following each link, I'll include a "short | 26 | # credit them in comments below. Further in the commentary on this script, I'll |
27 | # code" that I'll use to reference them in those comments, if necessary to keep | 27 | # include the following link numbers to refer to these documents, in order to |
28 | # them shorter than 80 characters. | 28 | # keep the line length as short as possible. |
29 | # | 29 | # |
30 | # [1]: https://github.com/dylanaraps/pure-bash-bible [PBB] | 30 | # [1]: Pure Bash Bible |
31 | # [2]: https://tools.ietf.org/html/rfc3986 [URLspec] | 31 | # https://github.com/dylanaraps/pure-bash-bible |
32 | # [3]: https://gemini.circumlunar.space/docs/specification.html [GEMspec] | 32 | # [2]: URL Specification |
33 | # [4]: https://tools.ietf.org/html/rfc1436 [GOPHERprotocol] | 33 | # https://tools.ietf.org/html/rfc3986 |
34 | # [5]: https://tools.ietf.org/html/rfc4266 [GOPHERurl] | 34 | # [3]: Gemini Specification |
35 | # [6]: [GOPHER_GEMINI]: | 35 | # https://gemini.circumlunar.space/docs/specification.html |
36 | # [4]: Gemini Best Practices | ||
37 | # https://gemini.circumlunar.space/docs/best-practices.gmi | ||
38 | # [5]: Gemini FAQ | ||
39 | # https://gemini.circumlunar.space/docs/faq.gmi | ||
40 | # [6]: Gopher Specification | ||
41 | # https://tools.ietf.org/html/rfc1436 | ||
42 | # [7]: Gopher URLs | ||
43 | # https://tools.ietf.org/html/rfc4266 | ||
44 | # [8]: Gophermap to Gemini script (by tomasino) | ||
36 | # https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk | 45 | # https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk |
46 | # [9]: OpenSSL `s_client' online manual | ||
47 | # https://www.openssl.org/docs/manmaster/man1/openssl-s_client.html | ||
37 | # | 48 | # |
38 | # Code: | 49 | # Code: |
39 | 50 | ||
@@ -90,14 +101,14 @@ trap bollux_quit SIGINT | |||
90 | 101 | ||
91 | # Bash built-in replacement for `sleep` | 102 | # Bash built-in replacement for `sleep` |
92 | # | 103 | # |
93 | # PBB: #use-read-as-an-alternative-to-the-sleep-command | 104 | # [1]: #use-read-as-an-alternative-to-the-sleep-command |
94 | sleep() { # sleep SECONDS | 105 | sleep() { # sleep SECONDS |
95 | read -rt "$1" <> <(:) || : | 106 | read -rt "$1" <> <(:) || : |
96 | } | 107 | } |
97 | 108 | ||
98 | # Trim leading and trailing whitespace from a string. | 109 | # Trim leading and trailing whitespace from a string. |
99 | # | 110 | # |
100 | # PBB: #trim-leading-and-trailing-white-space-from-string | 111 | # [1]: #trim-leading-and-trailing-white-space-from-string |
101 | trim_string() { # trim_string STRING | 112 | trim_string() { # trim_string STRING |
102 | : "${1#"${1%%[![:space:]]*}"}" | 113 | : "${1#"${1%%[![:space:]]*}"}" |
103 | : "${_%"${_##*[![:space:]]}"}" | 114 | : "${_%"${_##*[![:space:]]}"}" |
@@ -130,14 +141,14 @@ log() { # log LEVEL MESSAGE | |||
130 | local fmt | 141 | local fmt |
131 | 142 | ||
132 | case "$1" in | 143 | case "$1" in |
133 | [dD]*) # debug | 144 | ([dD]*) # debug |
134 | [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return | 145 | [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return |
135 | fmt=34 | 146 | fmt=34 |
136 | ;; | 147 | ;; |
137 | [eE]*) # error | 148 | ([eE]*) # error |
138 | fmt=31 | 149 | fmt=31 |
139 | ;; | 150 | ;; |
140 | *) fmt=1 ;; | 151 | (*) fmt=1 ;; |
141 | esac | 152 | esac |
142 | shift | 153 | shift |
143 | 154 | ||
@@ -190,14 +201,14 @@ bollux() { | |||
190 | bollux_args() { | 201 | bollux_args() { |
191 | while getopts :hvq OPT; do | 202 | while getopts :hvq OPT; do |
192 | case "$OPT" in | 203 | case "$OPT" in |
193 | h) | 204 | (h) |
194 | bollux_usage | 205 | bollux_usage |
195 | exit | 206 | exit |
196 | ;; | 207 | ;; |
197 | v) BOLLUX_LOGLEVEL=DEBUG ;; | 208 | (v) BOLLUX_LOGLEVEL=DEBUG ;; |
198 | q) BOLLUX_LOGLEVEL=QUIET ;; | 209 | (q) BOLLUX_LOGLEVEL=QUIET ;; |
199 | :) die 1 "Option -$OPTARG requires an argument" ;; | 210 | (:) die 1 "Option -$OPTARG requires an argument" ;; |
200 | *) die 1 "Unknown option: -$OPTARG" ;; | 211 | (*) die 1 "Unknown option: -$OPTARG" ;; |
201 | esac | 212 | esac |
202 | done | 213 | done |
203 | shift $((OPTIND - 1)) | 214 | shift $((OPTIND - 1)) |
@@ -337,7 +348,8 @@ blastoff() { # blastoff [-u] URL | |||
337 | } | 348 | } |
338 | } | 349 | } |
339 | 350 | ||
340 | # URLS: https://tools.ietf.org/html/rfc3986 #################################### | 351 | # URLS ######################################################################### |
352 | # https://tools.ietf.org/html/rfc3986 [2] | ||
341 | # | 353 | # |
342 | # Most of these functions are Bash implementations of functionality laid out in | 354 | # Most of these functions are Bash implementations of functionality laid out in |
343 | # the linked RFC specification. I'll refer to the section numbers above each | 355 | # the linked RFC specification. I'll refer to the section numbers above each |
@@ -369,8 +381,8 @@ uwellform() { | |||
369 | # Split a URL into its constituent parts, placing them all in the given array. | 381 | # Split a URL into its constituent parts, placing them all in the given array. |
370 | # | 382 | # |
371 | # The regular expression given at the top of the function ($re) is taken | 383 | # The regular expression given at the top of the function ($re) is taken |
372 | # directly from RFC 3986, Appendix B -- and if the URL provided doesn't match | 384 | # directly from [2] Appendix B -- and if the URL provided doesn't match it, the |
373 | # it, the function bails. | 385 | # function bails. |
374 | # | 386 | # |
375 | # `usplit' takes advantage of bash's regex abilities: when the regex comparison | 387 | # `usplit' takes advantage of bash's regex abilities: when the regex comparison |
376 | # operator `=~' is used, bash populates the array $BASH_REMATCH with the groups | 388 | # operator `=~' is used, bash populates the array $BASH_REMATCH with the groups |
@@ -432,8 +444,6 @@ ujoin() { # ujoin NAME:ARRAY | |||
432 | log d "${U[0]}" | 444 | log d "${U[0]}" |
433 | } | 445 | } |
434 | 446 | ||
435 | # Three small utility functions for dealing with URL components. | ||
436 | # | ||
437 | # `ucdef' checks whether a URL component is blank or not -- if a component | 447 | # `ucdef' checks whether a URL component is blank or not -- if a component |
438 | # doesn't exist, `usplit' writes $UC_BLANK there instead (which is :?: by | 448 | # doesn't exist, `usplit' writes $UC_BLANK there instead (which is :?: by |
439 | # default, though it really doesn't matter much *what* it is, as long as it's | 449 | # default, though it really doesn't matter much *what* it is, as long as it's |
@@ -458,6 +468,75 @@ ucset() { # ucset NAME VALUE | |||
458 | run ujoin "${1/\[*\]/}" | 468 | run ujoin "${1/\[*\]/}" |
459 | } | 469 | } |
460 | 470 | ||
471 | # [1]: encode a URL using percent-encoding. | ||
472 | uencode() { # uencode URL:STRING | ||
473 | local LC_ALL=C | ||
474 | for ((i = 0; i < ${#1}; i++)); do | ||
475 | : "${1:i:1}" | ||
476 | case "$_" in | ||
477 | ([a-zA-Z0-9.~_-]) printf '%s' "$_" ;; | ||
478 | (*) printf '%%%02X' "'$_" ;; | ||
479 | esac | ||
480 | done | ||
481 | printf '\n' | ||
482 | } | ||
483 | |||
484 | # [1]: decode a percent-encoded URL. | ||
485 | udecode() { # udecode URL:STRING | ||
486 | : "${1//+/ }" | ||
487 | printf '%b\n' "${_//%/\\x}" | ||
488 | } | ||
489 | |||
490 | # Implement [2] § 5.2.4, "Remove Dot Segments" | ||
491 | pundot() { # pundot PATH:STRING | ||
492 | local input="$1" | ||
493 | local output | ||
494 | while [[ "$input" ]]; do | ||
495 | if [[ "$input" =~ ^\.\.?/ ]]; then | ||
496 | input="${input#${BASH_REMATCH[0]}}" | ||
497 | elif [[ "$input" =~ ^/\.(/|$) ]]; then | ||
498 | input="/${input#${BASH_REMATCH[0]}}" | ||
499 | elif [[ "$input" =~ ^/\.\.(/|$) ]]; then | ||
500 | input="/${input#${BASH_REMATCH[0]}}" | ||
501 | [[ "$output" =~ /?[^/]+$ ]] | ||
502 | output="${output%${BASH_REMATCH[0]}}" | ||
503 | elif [[ "$input" == . || "$input" == .. ]]; then | ||
504 | input= | ||
505 | else | ||
506 | [[ $input =~ ^(/?[^/]*)(/?.*)$ ]] || return 1 | ||
507 | output="$output${BASH_REMATCH[1]}" | ||
508 | input="${BASH_REMATCH[2]}" | ||
509 | fi | ||
510 | done | ||
511 | printf '%s\n' "${output//\/\//\//}" | ||
512 | } | ||
513 | |||
514 | # Implement [2] § 5.2.3, "Merge Paths" | ||
515 | pmerge() { # pmerge BASE:ARRAY REFERENCE:ARRAY | ||
516 | local -n b="$1" | ||
517 | local -n r="$2" | ||
518 | |||
519 | if ucblank r[3]; then | ||
520 | printf '%s\n' "${b[3]//\/\//\//}" | ||
521 | return | ||
522 | fi | ||
523 | |||
524 | if ucdef b[2] && ucblank b[3]; then | ||
525 | printf '/%s\n' "${r[3]//\/\//\//}" | ||
526 | else | ||
527 | local bp="" | ||
528 | if [[ "${b[3]}" == */* ]]; then | ||
529 | bp="${b[3]%/*}" | ||
530 | fi | ||
531 | printf '%s/%s\n' "${bp%/}" "${r[3]#/}" | ||
532 | fi | ||
533 | } | ||
534 | |||
535 | # `utransform' implements [2]6 § 5.2.2, "Transform Resources." | ||
536 | # | ||
537 | # That section conveniently lays out a pseudocode algorithm describing how URL | ||
538 | # resources should be transformed from one to another. This function just | ||
539 | # implements that pseudocode in Bash, using the helper functions defined above. | ||
461 | utransform() { # utransform TARGET:ARRAY BASE:STRING REFERENCE:STRING | 540 | utransform() { # utransform TARGET:ARRAY BASE:STRING REFERENCE:STRING |
462 | local -a B R # base, reference | 541 | local -a B R # base, reference |
463 | local -n T="$1" # target | 542 | local -n T="$1" # target |
@@ -520,128 +599,136 @@ utransform() { # utransform TARGET:ARRAY BASE:STRING REFERENCE:STRING | |||
520 | ujoin T | 599 | ujoin T |
521 | } | 600 | } |
522 | 601 | ||
523 | pundot() { # pundot PATH:STRING | 602 | # GEMINI ####################################################################### |
524 | local input="$1" | 603 | # https://gemini.circumlunar.space/docs/specification.html [3] |
525 | local output | 604 | # |
526 | while [[ "$input" ]]; do | 605 | # The reason we're all here, folks. Gemini is a new protocol that aims to be a |
527 | if [[ "$input" =~ ^\.\.?/ ]]; then | 606 | # middle ground between Gopher and HTTP, blah blah. You know the spiel. I know |
528 | input="${input#${BASH_REMATCH[0]}}" | 607 | # the spiel. It's great stuff! |
529 | elif [[ "$input" =~ ^/\.(/|$) ]]; then | 608 | # |
530 | input="/${input#${BASH_REMATCH[0]}}" | 609 | ################################################################################ |
531 | elif [[ "$input" =~ ^/\.\.(/|$) ]]; then | ||
532 | input="/${input#${BASH_REMATCH[0]}}" | ||
533 | [[ "$output" =~ /?[^/]+$ ]] | ||
534 | output="${output%${BASH_REMATCH[0]}}" | ||
535 | elif [[ "$input" == . || "$input" == .. ]]; then | ||
536 | input= | ||
537 | else | ||
538 | [[ $input =~ ^(/?[^/]*)(/?.*)$ ]] || return 1 | ||
539 | output="$output${BASH_REMATCH[1]}" | ||
540 | input="${BASH_REMATCH[2]}" | ||
541 | fi | ||
542 | done | ||
543 | printf '%s\n' "${output//\/\//\//}" | ||
544 | } | ||
545 | |||
546 | pmerge() { | ||
547 | local -n b="$1" | ||
548 | local -n r="$2" | ||
549 | |||
550 | if ucblank r[3]; then | ||
551 | printf '%s\n' "${b[3]//\/\//\//}" | ||
552 | return | ||
553 | fi | ||
554 | |||
555 | if ucdef b[2] && ucblank b[3]; then | ||
556 | printf '/%s\n' "${r[3]//\/\//\//}" | ||
557 | else | ||
558 | local bp="" | ||
559 | if [[ "${b[3]}" == */* ]]; then | ||
560 | bp="${b[3]%/*}" | ||
561 | fi | ||
562 | printf '%s/%s\n' "${bp%/}" "${r[3]#/}" | ||
563 | fi | ||
564 | } | ||
565 | |||
566 | # PBB | ||
567 | uencode() { # uencode URL:STRING | ||
568 | local LC_ALL=C | ||
569 | for ((i = 0; i < ${#1}; i++)); do | ||
570 | : "${1:i:1}" | ||
571 | case "$_" in | ||
572 | [a-zA-Z0-9.~_-]) | ||
573 | printf '%s' "$_" | ||
574 | ;; | ||
575 | *) | ||
576 | printf '%%%02X' "'$_" | ||
577 | ;; | ||
578 | esac | ||
579 | done | ||
580 | printf '\n' | ||
581 | } | ||
582 | |||
583 | # PBB | ||
584 | udecode() { # udecode URL:STRING | ||
585 | : "${1//+/ }" | ||
586 | printf '%b\n' "${_//%/\\x}" | ||
587 | } | ||
588 | 610 | ||
589 | # GEMINI | 611 | # Request a resource from a gemini server - see [3] §§ 2, 4. |
590 | # https://gemini.circumlunar.space/docs/specification.html | ||
591 | gemini_request() { # gemini_request URL | 612 | gemini_request() { # gemini_request URL |
592 | local -a url | 613 | local -a url |
593 | usplit url "$1" | 614 | usplit url "$1" |
594 | 615 | ||
595 | # get rid of userinfo | 616 | # Remove user info from the URL. |
617 | # | ||
618 | # URLs can technically be of the form <proto>://<user>:<pass>@<domain> | ||
619 | # (see [2], § 3.2, "Authority"). I don't know of any Gemini servers | ||
620 | # that use the <user> or <pass> parts, so `gemini_request' just strips | ||
621 | # them from the requested URL. This will need to be changed if servers | ||
622 | # decide to use this method of authentication. | ||
596 | ucset url[2] "${url[2]#*@}" | 623 | ucset url[2] "${url[2]#*@}" |
597 | 624 | ||
625 | # Determine the port to request. | ||
626 | # | ||
627 | # The default port for Gemini is 1965 (the year of the first Gemini | ||
628 | # space mission), but some servers use a different port. In a URL, a | ||
629 | # port can be specified after the domain, separated with a colon. The | ||
630 | # user can also request a different default port, for whatever reason, | ||
631 | # by setting the variable $BOLLUX_GEMINI_PORT. | ||
598 | local port | 632 | local port |
599 | if [[ "${url[2]}" == *:* ]]; then | 633 | if [[ "${url[2]}" == *:* ]]; then |
600 | port="${url[2]#*:}" | 634 | port="${url[2]#*:}" |
601 | ucset url[2] "${url[2]%:*}" | 635 | ucset url[2] "${url[2]%:*}" |
602 | else | 636 | else |
603 | port=1965 # TODO variablize | 637 | port="$BOLLUX_GEMINI_PORT" |
604 | fi | 638 | fi |
605 | 639 | ||
640 | |||
641 | # Build the SSL command to request the resource. | ||
642 | # | ||
643 | # This is the beating heart of bollux, the command that does all the | ||
644 | # important work of actually fetching the gemini content the user wants | ||
645 | # to read. I've broken it out into an array for ease of editing (and | ||
646 | # now, commenting!). | ||
606 | local ssl_cmd=( | 647 | local ssl_cmd=( |
648 | # `s_client' is OpenSSL's reference client implementation In the | ||
649 | # manual [9] it says not to use it, but who reads the manual, | ||
650 | # anyway? | ||
607 | openssl s_client | 651 | openssl s_client |
608 | -crlf -quiet -connect "${url[2]}:$port" | 652 | -crlf # Automatically add CR+LF to line |
609 | -servername "${url[2]}" # SNI | 653 | -quiet # Don't print all the cert stuff |
610 | -no_ssl3 -no_tls1 -no_tls1_1 # disable old TLS/SSL versions | 654 | # -ign_eof # `-quiet' implies `-ign_eof' |
655 | -connect "${url[2]}:$port" # The server and port to connect | ||
656 | -servername "${url[2]}" # SNI: Server Name Identification | ||
657 | -no_ssl3 -no_tls1 -no_tls1_1 # disable old TLS/SSL versions | ||
611 | ) | 658 | ) |
612 | 659 | ||
660 | # Actually request the resource. | ||
661 | # | ||
662 | # I could probably use 'printf '%s\r\n' "$url" | run "${ssl_cmd[@]}", | ||
663 | # and maybe I should. I wrote this little line a while ago. | ||
613 | run "${ssl_cmd[@]}" <<<"$url" | 664 | run "${ssl_cmd[@]}" <<<"$url" |
614 | } | 665 | } |
615 | 666 | ||
667 | # Handle the gemini response - see [3] § 3. | ||
616 | gemini_response() { # gemini_response URL | 668 | gemini_response() { # gemini_response URL |
617 | local url code meta | 669 | local code meta # received on the first line of the response |
618 | local title | 670 | local title # determined by a clunky heuristic, see read loop: (2*) |
619 | url="$1" | 671 | local url="$1" # the currently-visited URL. |
620 | 672 | ||
621 | # we need a loop here so it waits for the first line | 673 | # Read the first line. |
674 | # | ||
675 | # The first line of a Gemini response is the "header line," which is of | ||
676 | # the format "STATUS METADATA\r\n". I use a `while' loop using `read' | ||
677 | # with a timeout to handle non-responsive servers. Technically, | ||
678 | # METADATA shouldn't exceed 1024 bytes, but I can't think of a good way | ||
679 | # to break at that point -- so bollux is not quite spec-compliant in | ||
680 | # this regard. | ||
681 | # | ||
682 | # Additionally, there are sometimes bugs with caching and | ||
683 | # byte-shifting(?) when trying to download a binary file (see | ||
684 | # `download', below), but I'm not sure how to remedy that issue either. | ||
685 | # It requires more research. | ||
622 | while read -t "$BOLLUX_TIMEOUT" -r code meta || | 686 | while read -t "$BOLLUX_TIMEOUT" -r code meta || |
623 | { (($? > 128)) && die 99 "Timeout."; }; do | 687 | { (($? > 128)) && die 99 "Timeout."; }; do |
624 | break | 688 | break |
625 | done | 689 | done |
626 | |||
627 | log d "[$code] $meta" | 690 | log d "[$code] $meta" |
628 | 691 | ||
692 | # Branch depending on the status code. See [3], Appendix 1. | ||
693 | # | ||
694 | # Notes: | ||
695 | # - All codes other than 3* (Redirects) reset the REDIRECTS counter. | ||
696 | # - I branch on the first digit of the status code, instead of both, to | ||
697 | # minimize the amount of duplicated code I need to write. | ||
629 | case "$code" in | 698 | case "$code" in |
630 | 1*) # input | 699 | (1*) # INPUT |
700 | # Gemini allows GET-style requests, and the INPUT family of | ||
701 | # response codes facilitate them. `10' is for standard input, | ||
702 | # and `11' is for sensitive information, like passwords. | ||
631 | REDIRECTS=0 | 703 | REDIRECTS=0 |
632 | BOLLUX_URL="$url" | 704 | BOLLUX_URL="$url" |
633 | case "$code" in | 705 | case "$code" in |
634 | 10) run prompt "$meta" ;; | 706 | (10) run prompt "$meta" ;; |
635 | 11) run prompt "$meta" -s ;; # password input | 707 | (11) run prompt "$meta" -s ;; # sensitive input |
636 | esac | 708 | esac |
709 | run history_append "$url" "${title:-}" | ||
637 | run blastoff "?$(uencode "$REPLY")" | 710 | run blastoff "?$(uencode "$REPLY")" |
638 | ;; | 711 | ;; |
639 | 2*) # OK | 712 | (2*) # OK |
713 | # The `20' family of requests is like HTTP's `200' family: it | ||
714 | # means that the request worked and the server is sending the | ||
715 | # requested content. | ||
640 | REDIRECTS=0 | 716 | REDIRECTS=0 |
641 | BOLLUX_URL="$url" | 717 | BOLLUX_URL="$url" |
642 | # read ahead to find a title | 718 | # Janky heuristic to guess the title of a page. |
719 | # | ||
720 | # This while loop reads through the file looking for a line | ||
721 | # starting with `#', which is a level-one heading in text/gemini | ||
722 | # (see [3], § 5). It assumes that the first such heading is the | ||
723 | # title of the page, and uses that title for the terminal title | ||
724 | # and for the history. | ||
643 | local pretitle | 725 | local pretitle |
644 | while read -r; do | 726 | while read -r; do |
727 | # Since looping through the file consumes it (that is, | ||
728 | # the file pointer (I think?) moves away from the | ||
729 | # beginning of the file), the content we've read so far | ||
730 | # must be saved in a `pretitle' variable, so it can be | ||
731 | # printed later with the rest of the page. | ||
645 | pretitle="$pretitle$REPLY"$'\n' | 732 | pretitle="$pretitle$REPLY"$'\n' |
646 | if [[ "$REPLY" =~ ^#[[:space:]]*(.*) ]]; then | 733 | if [[ "$REPLY" =~ ^#[[:space:]]*(.*) ]]; then |
647 | title="${BASH_REMATCH[1]}" | 734 | title="${BASH_REMATCH[1]}" |
@@ -649,35 +736,55 @@ gemini_response() { # gemini_response URL | |||
649 | fi | 736 | fi |
650 | done | 737 | done |
651 | run history_append "$url" "${title:-}" | 738 | run history_append "$url" "${title:-}" |
652 | # read the body out and pipe it to display | 739 | # Print the pretitle and the rest of the document (`passthru' is |
740 | # a pure-bash rewrite of `cat'), and pipe it through `display' | ||
741 | # for typesetting. | ||
653 | { | 742 | { |
654 | printf '%s' "$pretitle" | 743 | printf '%s' "$pretitle" |
655 | passthru | 744 | passthru |
656 | } | run display "$meta" "${title:-}" | 745 | } | run display "$meta" "${title:-}" |
657 | ;; | 746 | ;; |
658 | 3*) # redirect | 747 | (3*) # REDIRECT |
748 | # Redirects are a fundamental part of any hypertext framework, | ||
749 | # and if I remember correctly, one of the main reasons | ||
750 | # solderpunk and others began thinking about gemini (the others | ||
751 | # being TLS and URLs, I believe). | ||
752 | # | ||
753 | # Note that although [3] specifies both a temporary (30) and | ||
754 | # permanent (31) redirect, bollux isn't smart enough to make a | ||
755 | # distinction. I'm not sure what the difference would be in | ||
756 | # practice, anyway. | ||
757 | # | ||
758 | # Per [4], bollux limits the number of redirects a page is | ||
759 | # allowed to make (by default, five). Change `$BOLLUX_MAXREDIR' | ||
760 | # to customize that limit. | ||
659 | ((REDIRECTS += 1)) | 761 | ((REDIRECTS += 1)) |
660 | if ((REDIRECTS > BOLLUX_MAXREDIR)); then | 762 | if ((REDIRECTS > BOLLUX_MAXREDIR)); then |
661 | die $((100 + code)) "Too many redirects!" | 763 | die $((100 + code)) "Too many redirects!" |
662 | fi | 764 | fi |
663 | BOLLUX_URL="$url" | 765 | BOLLUX_URL="$url" |
766 | # Another discussion on [4] pertains to the value of alerting | ||
767 | # the user to (A) a cross-domain redirect, or even (B) all | ||
768 | # redirects. I have yet to implement that particular | ||
769 | # functionality, and even when I do implement it I don't think | ||
770 | # (B) will be the default. Perhaps (A) though. No notification | ||
771 | # will also be an option, however. | ||
664 | run blastoff "$meta" # TODO: confirm redirect | 772 | run blastoff "$meta" # TODO: confirm redirect |
665 | ;; | 773 | ;; |
666 | 4*) # temporary error | 774 | (4*) # TEMPORARY ERROR |
667 | REDIRECTS=0 | 775 | REDIRECTS=0 |
668 | die "$((100 + code))" "Temporary error [$code]: $meta" | 776 | die "$((100 + code))" "Temporary error [$code]: $meta" |
669 | ;; | 777 | ;; |
670 | 5*) # permanent error | 778 | (5*) # PERMANENT ERROR |
671 | REDIRECTS=0 | 779 | REDIRECTS=0 |
672 | die "$((100 + code))" "Permanent error [$code]: $meta" | 780 | die "$((100 + code))" "Permanent error [$code]: $meta" |
673 | ;; | 781 | ;; |
674 | 6*) # certificate error | 782 | (6*) # CERTIFICATE ERROR |
675 | REDIRECTS=0 | 783 | REDIRECTS=0 |
676 | log d "Not implemented: Client certificates" | 784 | log d "Not implemented: Client certificates" |
677 | # TODO: recheck the speck | ||
678 | die "$((100 + code))" "[$code] $meta" | 785 | die "$((100 + code))" "[$code] $meta" |
679 | ;; | 786 | ;; |
680 | *) | 787 | (*) |
681 | [[ -z "${code-}" ]] && die 100 "Empty response code." | 788 | [[ -z "${code-}" ]] && die 100 "Empty response code." |
682 | die "$((100 + code))" "Unknown response code: $code." | 789 | die "$((100 + code))" "Unknown response code: $code." |
683 | ;; | 790 | ;; |
@@ -720,16 +827,16 @@ gopher_response() { # gopher_response URL | |||
720 | log d "TYPE='$type'" | 827 | log d "TYPE='$type'" |
721 | 828 | ||
722 | case "$type" in | 829 | case "$type" in |
723 | 0) # text | 830 | (0) # text |
724 | run display text/plain | 831 | run display text/plain |
725 | ;; | 832 | ;; |
726 | 1) # menu | 833 | (1) # menu |
727 | run gopher_convert | run display text/gemini | 834 | run gopher_convert | run display text/gemini |
728 | ;; | 835 | ;; |
729 | 3) # failure | 836 | (3) # failure |
730 | die 203 "GOPHER: failed" | 837 | die 203 "GOPHER: failed" |
731 | ;; | 838 | ;; |
732 | 7) # search | 839 | (7) # search |
733 | if [[ "$url" =~ $'\t' ]]; then | 840 | if [[ "$url" =~ $'\t' ]]; then |
734 | run gopher_convert | run display text/gemini | 841 | run gopher_convert | run display text/gemini |
735 | else | 842 | else |
@@ -737,19 +844,12 @@ gopher_response() { # gopher_response URL | |||
737 | run blastoff "$url $REPLY" | 844 | run blastoff "$url $REPLY" |
738 | fi | 845 | fi |
739 | ;; | 846 | ;; |
740 | *) # something else | 847 | (*) # something else |
741 | run download "$url" | 848 | run download "$url" |
742 | ;; | 849 | ;; |
743 | esac | 850 | esac |
744 | } | 851 | } |
745 | 852 | ||
746 | # 'cat' but in pure bash | ||
747 | passthru() { | ||
748 | while IFS= read -r; do | ||
749 | printf '%s\n' "$REPLY" | ||
750 | done | ||
751 | } | ||
752 | |||
753 | # convert gophermap to text/gemini (probably naive) | 853 | # convert gophermap to text/gemini (probably naive) |
754 | gopher_convert() { | 854 | gopher_convert() { |
755 | local type label path server port regex | 855 | local type label path server port regex |
@@ -768,19 +868,19 @@ gopher_convert() { | |||
768 | continue | 868 | continue |
769 | fi | 869 | fi |
770 | case "$type" in | 870 | case "$type" in |
771 | .) # end of file | 871 | (.) # end of file |
772 | printf '.\n' | 872 | printf '.\n' |
773 | break | 873 | break |
774 | ;; | 874 | ;; |
775 | i) # label | 875 | (i) # label |
776 | case "$label" in | 876 | case "$label" in |
777 | '#'* | '*'[[:space:]]*) | 877 | ('#'* | '*'[[:space:]]*) |
778 | if $pre; then | 878 | if $pre; then |
779 | printf '%s\n' '```' | 879 | printf '%s\n' '```' |
780 | pre=false | 880 | pre=false |
781 | fi | 881 | fi |
782 | ;; | 882 | ;; |
783 | *) | 883 | (*) |
784 | if ! $pre; then | 884 | if ! $pre; then |
785 | printf '%s\n' '```' | 885 | printf '%s\n' '```' |
786 | pre=true | 886 | pre=true |
@@ -789,14 +889,14 @@ gopher_convert() { | |||
789 | esac | 889 | esac |
790 | printf '%s\n' "$label" | 890 | printf '%s\n' "$label" |
791 | ;; | 891 | ;; |
792 | h) # html link | 892 | (h) # html link |
793 | if $pre; then | 893 | if $pre; then |
794 | printf '%s\n' '```' | 894 | printf '%s\n' '```' |
795 | pre=false | 895 | pre=false |
796 | fi | 896 | fi |
797 | printf '=> %s %s\n' "${path:4}" "$label" | 897 | printf '=> %s %s\n' "${path:4}" "$label" |
798 | ;; | 898 | ;; |
799 | T) # telnet link | 899 | (T) # telnet link |
800 | if $pre; then | 900 | if $pre; then |
801 | printf '%s\n' '```' | 901 | printf '%s\n' '```' |
802 | pre=false | 902 | pre=false |
@@ -804,7 +904,7 @@ gopher_convert() { | |||
804 | printf '=> telnet://%s:%s/%s%s %s\n' \ | 904 | printf '=> telnet://%s:%s/%s%s %s\n' \ |
805 | "$server" "$port" "$type" "$path" "$label" | 905 | "$server" "$port" "$type" "$path" "$label" |
806 | ;; | 906 | ;; |
807 | *) # other type | 907 | (*) # other type |
808 | if $pre; then | 908 | if $pre; then |
809 | printf '%s\n' '```' | 909 | printf '%s\n' '```' |
810 | pre=false | 910 | pre=false |
@@ -822,6 +922,14 @@ gopher_convert() { | |||
822 | exec 9>&- | 922 | exec 9>&- |
823 | } | 923 | } |
824 | 924 | ||
925 | |||
926 | # 'cat' but in pure bash | ||
927 | passthru() { | ||
928 | while IFS= read -r; do | ||
929 | printf '%s\n' "$REPLY" | ||
930 | done | ||
931 | } | ||
932 | |||
825 | # display the fetched content | 933 | # display the fetched content |
826 | display() { # display METADATA [TITLE] | 934 | display() { # display METADATA [TITLE] |
827 | local -a less_cmd | 935 | local -a less_cmd |
@@ -839,7 +947,7 @@ display() { # display METADATA [TITLE] | |||
839 | for ((i = 1; i <= "${#hdr[@]}"; i++)); do | 947 | for ((i = 1; i <= "${#hdr[@]}"; i++)); do |
840 | h="${hdr[$i]}" | 948 | h="${hdr[$i]}" |
841 | case "$h" in | 949 | case "$h" in |
842 | *charset=*) charset="${h#*=}" ;; | 950 | (*charset=*) charset="${h#*=}" ;; |
843 | esac | 951 | esac |
844 | done | 952 | done |
845 | 953 | ||
@@ -849,7 +957,7 @@ display() { # display METADATA [TITLE] | |||
849 | log debug "mime='$mime'; charset='$charset'" | 957 | log debug "mime='$mime'; charset='$charset'" |
850 | 958 | ||
851 | case "$mime" in | 959 | case "$mime" in |
852 | text/*) | 960 | (text/*) |
853 | set_title "$title${title:+ - }bollux" | 961 | set_title "$title${title:+ - }bollux" |
854 | # render ANSI color escapes and don't wrap pre-formatted blocks | 962 | # render ANSI color escapes and don't wrap pre-formatted blocks |
855 | less_cmd=(less -RS) | 963 | less_cmd=(less -RS) |
@@ -886,7 +994,7 @@ display() { # display METADATA [TITLE] | |||
886 | run "${less_cmd[@]}" && bollux_quit | 994 | run "${less_cmd[@]}" && bollux_quit |
887 | } || run handle_keypress "$?" | 995 | } || run handle_keypress "$?" |
888 | ;; | 996 | ;; |
889 | *) run download "$BOLLUX_URL" ;; | 997 | (*) run download "$BOLLUX_URL" ;; |
890 | esac | 998 | esac |
891 | } | 999 | } |
892 | 1000 | ||
@@ -896,8 +1004,8 @@ less_prompt_escape() { # less_prompt_escape STRING | |||
896 | for ((i = 0; i < ${#1}; i++)); do | 1004 | for ((i = 0; i < ${#1}; i++)); do |
897 | : "${1:i:1}" | 1005 | : "${1:i:1}" |
898 | case "$_" in | 1006 | case "$_" in |
899 | [\?:\.%\\]) printf '\%s' "$_" ;; | 1007 | ([\?:\.%\\]) printf '\%s' "$_" ;; |
900 | *) printf '%s' "$_" ;; | 1008 | (*) printf '%s' "$_" ;; |
901 | esac | 1009 | esac |
902 | done | 1010 | done |
903 | printf '\n' | 1011 | printf '\n' |
@@ -965,7 +1073,7 @@ typeset_gemini() { | |||
965 | 1073 | ||
966 | while IFS= read -r; do | 1074 | while IFS= read -r; do |
967 | case "$REPLY" in | 1075 | case "$REPLY" in |
968 | '```'*) | 1076 | ('```'*) |
969 | PRE_LINE_FORCE=false | 1077 | PRE_LINE_FORCE=false |
970 | if $pre; then | 1078 | if $pre; then |
971 | pre=false | 1079 | pre=false |
@@ -973,28 +1081,28 @@ typeset_gemini() { | |||
973 | pre=true | 1081 | pre=true |
974 | fi | 1082 | fi |
975 | case "${T_PRE_DISPLAY%%,*}" in | 1083 | case "${T_PRE_DISPLAY%%,*}" in |
976 | pre) | 1084 | (pre) |
977 | : | 1085 | : |
978 | ;; | 1086 | ;; |
979 | alt | both) | 1087 | (alt | both) |
980 | $pre && PRE_LINE_FORCE=true \ | 1088 | $pre && PRE_LINE_FORCE=true \ |
981 | gemini_pre "${REPLY#\`\`\`}" | 1089 | gemini_pre "${REPLY#\`\`\`}" |
982 | ;; | 1090 | ;; |
983 | esac | 1091 | esac |
984 | continue | 1092 | continue |
985 | ;; | 1093 | ;; |
986 | '=>'*) | 1094 | ('=>'*) |
987 | : $((ln += 1)) | 1095 | : $((ln += 1)) |
988 | gemini_link "$REPLY" $pre "$ln" | 1096 | gemini_link "$REPLY" $pre "$ln" |
989 | ;; | 1097 | ;; |
990 | '#'*) gemini_header "$REPLY" $pre ;; | 1098 | ('#'*) gemini_header "$REPLY" $pre ;; |
991 | '*'[[:space:]]*) | 1099 | ('*'[[:space:]]*) |
992 | gemini_list "$REPLY" $pre | 1100 | gemini_list "$REPLY" $pre |
993 | ;; | 1101 | ;; |
994 | '>'*) | 1102 | ('>'*) |
995 | gemini_quote "$REPLY" $pre | 1103 | gemini_quote "$REPLY" $pre |
996 | ;; | 1104 | ;; |
997 | *) gemini_text "$REPLY" $pre ;; | 1105 | (*) gemini_text "$REPLY" $pre ;; |
998 | esac | 1106 | esac |
999 | done | 1107 | done |
1000 | } | 1108 | } |
@@ -1103,25 +1211,25 @@ fold_line() { # fold_line [OPTIONS...] WIDTH TEXT | |||
1103 | OPTIND=0 | 1211 | OPTIND=0 |
1104 | while getopts nm:f:l:B:A: OPT; do | 1212 | while getopts nm:f:l:B:A: OPT; do |
1105 | case "$OPT" in | 1213 | case "$OPT" in |
1106 | n) # -n = no trailing newline | 1214 | (n) # -n = no trailing newline |
1107 | newline=false | 1215 | newline=false |
1108 | ;; | 1216 | ;; |
1109 | m) # -m MARGIN = margin for all lines | 1217 | (m) # -m MARGIN = margin for all lines |
1110 | margin_all="$OPTARG" | 1218 | margin_all="$OPTARG" |
1111 | ;; | 1219 | ;; |
1112 | f) # -f MARGIN = margin for first line | 1220 | (f) # -f MARGIN = margin for first line |
1113 | margin_first="$OPTARG" | 1221 | margin_first="$OPTARG" |
1114 | ;; | 1222 | ;; |
1115 | l) # -l LENGTH = length of line before starting fold | 1223 | (l) # -l LENGTH = length of line before starting fold |
1116 | ll="$OPTARG" | 1224 | ll="$OPTARG" |
1117 | ;; | 1225 | ;; |
1118 | B) # -B BEFORE = text to insert before each line | 1226 | (B) # -B BEFORE = text to insert before each line |
1119 | before="$OPTARG" | 1227 | before="$OPTARG" |
1120 | ;; | 1228 | ;; |
1121 | A) # -A AFTER = text to insert after each line | 1229 | (A) # -A AFTER = text to insert after each line |
1122 | after="$OPTARG" | 1230 | after="$OPTARG" |
1123 | ;; | 1231 | ;; |
1124 | *) return 1 ;; | 1232 | (*) return 1 ;; |
1125 | esac | 1233 | esac |
1126 | done | 1234 | done |
1127 | shift "$((OPTIND - 1))" | 1235 | shift "$((OPTIND - 1))" |
@@ -1159,37 +1267,37 @@ fold_line() { # fold_line [OPTIONS...] WIDTH TEXT | |||
1159 | # use the exit code from less (see mklesskey) to do things | 1267 | # use the exit code from less (see mklesskey) to do things |
1160 | handle_keypress() { # handle_keypress CODE | 1268 | handle_keypress() { # handle_keypress CODE |
1161 | case "$1" in | 1269 | case "$1" in |
1162 | 48) # o - open a link -- show a menu of links on the page | 1270 | (48) # o - open a link -- show a menu of links on the page |
1163 | run select_url "$BOLLUX_PAGESRC" | 1271 | run select_url "$BOLLUX_PAGESRC" |
1164 | ;; | 1272 | ;; |
1165 | 49) # g - goto a url -- input a new url | 1273 | (49) # g - goto a url -- input a new url |
1166 | prompt GO | 1274 | prompt GO |
1167 | run blastoff -u "$REPLY" | 1275 | run blastoff -u "$REPLY" |
1168 | ;; | 1276 | ;; |
1169 | 50) # [ - back in the history | 1277 | (50) # [ - back in the history |
1170 | run history_back || { | 1278 | run history_back || { |
1171 | sleep 0.5 | 1279 | sleep 0.5 |
1172 | run blastoff "$BOLLUX_URL" | 1280 | run blastoff "$BOLLUX_URL" |
1173 | } | 1281 | } |
1174 | ;; | 1282 | ;; |
1175 | 51) # ] - forward in the history | 1283 | (51) # ] - forward in the history |
1176 | run history_forward || { | 1284 | run history_forward || { |
1177 | sleep 0.5 | 1285 | sleep 0.5 |
1178 | run blastoff "$BOLLUX_URL" | 1286 | run blastoff "$BOLLUX_URL" |
1179 | } | 1287 | } |
1180 | ;; | 1288 | ;; |
1181 | 52) # r - re-request the current resource | 1289 | (52) # r - re-request the current resource |
1182 | run blastoff "$BOLLUX_URL" | 1290 | run blastoff "$BOLLUX_URL" |
1183 | ;; | 1291 | ;; |
1184 | 53) # G - goto a url (pre-filled with current) | 1292 | (53) # G - goto a url (pre-filled with current) |
1185 | run prompt -u GO | 1293 | run prompt -u GO |
1186 | run blastoff -u "$REPLY" | 1294 | run blastoff -u "$REPLY" |
1187 | ;; | 1295 | ;; |
1188 | 54) # ` - change alt-text visibility and refresh | 1296 | (54) # ` - change alt-text visibility and refresh |
1189 | run cycle_list T_PRE_DISPLAY , | 1297 | run cycle_list T_PRE_DISPLAY , |
1190 | run blastoff "$BOLLUX_URL" | 1298 | run blastoff "$BOLLUX_URL" |
1191 | ;; | 1299 | ;; |
1192 | 55) # 55-57 -- still available for binding | 1300 | (55) # 55-57 -- still available for binding |
1193 | die "$?" "less(1) error" | 1301 | die "$?" "less(1) error" |
1194 | ;; | 1302 | ;; |
1195 | esac | 1303 | esac |
@@ -1206,8 +1314,8 @@ select_url() { # select_url FILE | |||
1206 | PS3="OPEN> " | 1314 | PS3="OPEN> " |
1207 | select u in "${MAPFILE[@]}"; do | 1315 | select u in "${MAPFILE[@]}"; do |
1208 | case "$REPLY" in | 1316 | case "$REPLY" in |
1209 | q) bollux_quit ;; | 1317 | (q) bollux_quit ;; |
1210 | [^0-9]*) run blastoff -u "$REPLY" && break ;; | 1318 | ([^0-9]*) run blastoff -u "$REPLY" && break ;; |
1211 | esac | 1319 | esac |
1212 | run blastoff "${u%%[[:space:]]*}" && break | 1320 | run blastoff "${u%%[[:space:]]*}" && break |
1213 | done </dev/tty | 1321 | done </dev/tty |