diff options
-rwxr-xr-x | bollux | 105 |
1 files changed, 76 insertions, 29 deletions
diff --git a/bollux b/bollux index 1489147..63e2afb 100755 --- a/bollux +++ b/bollux | |||
@@ -24,26 +24,26 @@ parameters: | |||
24 | END | 24 | END |
25 | } | 25 | } |
26 | 26 | ||
27 | run() { | 27 | run() { # run COMMAND... |
28 | log debug "$@" | 28 | log debug "$*" |
29 | "$@" | 29 | "$@" |
30 | } | 30 | } |
31 | 31 | ||
32 | die() { | 32 | die() { # die EXIT_CODE MESSAGE |
33 | local ec="$1" | 33 | local ec="$1" |
34 | shift | 34 | shift |
35 | log error "$*" | 35 | log error "$*" |
36 | exit "$ec" | 36 | exit "$ec" |
37 | } | 37 | } |
38 | 38 | ||
39 | # pure bash bible trim_string | 39 | # https://github.com/dylanaraps/pure-bash-bible/ |
40 | trim() { | 40 | trim_string() { # trim_string STRING |
41 | : "${1#"${1%%[![:space:]]*}"}" | 41 | : "${1#"${1%%[![:space:]]*}"}" |
42 | : "${_%"${_##*[![:space:]]}"}" | 42 | : "${_%"${_##*[![:space:]]}"}" |
43 | printf '%s\n' "$_" | 43 | printf '%s\n' "$_" |
44 | } | 44 | } |
45 | 45 | ||
46 | log() { | 46 | log() { # log LEVEL MESSAGE |
47 | [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return | 47 | [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return |
48 | local fmt | 48 | local fmt |
49 | 49 | ||
@@ -72,9 +72,12 @@ bollux() { | |||
72 | run prompt GO BOLLUX_URL | 72 | run prompt GO BOLLUX_URL |
73 | fi | 73 | fi |
74 | 74 | ||
75 | log d "BOLLUX_URL='$BOLLUX_URL'" | ||
76 | |||
75 | run blastoff "$BOLLUX_URL" | 77 | run blastoff "$BOLLUX_URL" |
76 | } | 78 | } |
77 | 79 | ||
80 | # process command-line arguments | ||
78 | bollux_args() { | 81 | bollux_args() { |
79 | while getopts :hvq OPT; do | 82 | while getopts :hvq OPT; do |
80 | case "$OPT" in | 83 | case "$OPT" in |
@@ -94,6 +97,7 @@ bollux_args() { | |||
94 | fi | 97 | fi |
95 | } | 98 | } |
96 | 99 | ||
100 | # process config file and set variables | ||
97 | bollux_config() { | 101 | bollux_config() { |
98 | : "${BOLLUX_CONFIG:=${XDG_CONFIG_DIR:-$HOME/.config}/bollux/bollux.conf}" | 102 | : "${BOLLUX_CONFIG:=${XDG_CONFIG_DIR:-$HOME/.config}/bollux/bollux.conf}" |
99 | 103 | ||
@@ -133,15 +137,18 @@ bollux_config() { | |||
133 | : "${C_PRE:=0}" # preformatted text formatting | 137 | : "${C_PRE:=0}" # preformatted text formatting |
134 | } | 138 | } |
135 | 139 | ||
140 | # quit happily | ||
136 | bollux_quit() { | 141 | bollux_quit() { |
137 | log x "$BOLLUX_BYEMSG" | 142 | log x "$BOLLUX_BYEMSG" |
138 | exit | 143 | exit |
139 | } | 144 | } |
140 | 145 | ||
141 | set_title() { | 146 | # set the terminal title |
147 | set_title() { # set_title STRING | ||
142 | printf '\e]2;%s\007' "$*" | 148 | printf '\e]2;%s\007' "$*" |
143 | } | 149 | } |
144 | 150 | ||
151 | # prompt for input | ||
145 | prompt() { # prompt [-u] PROMPT [READ_ARGS...] | 152 | prompt() { # prompt [-u] PROMPT [READ_ARGS...] |
146 | local read_cmd=(read -e -r) | 153 | local read_cmd=(read -e -r) |
147 | if [[ "$1" == "-u" ]]; then | 154 | if [[ "$1" == "-u" ]]; then |
@@ -154,7 +161,8 @@ prompt() { # prompt [-u] PROMPT [READ_ARGS...] | |||
154 | "${read_cmd[@]}" </dev/tty "$@" | 161 | "${read_cmd[@]}" </dev/tty "$@" |
155 | } | 162 | } |
156 | 163 | ||
157 | blastoff() { # load a url | 164 | # load a URL |
165 | blastoff() { # blastoff [-u] URL | ||
158 | local well_formed=true | 166 | local well_formed=true |
159 | local proto url | 167 | local proto url |
160 | if [[ "$1" == "-u" ]]; then | 168 | if [[ "$1" == "-u" ]]; then |
@@ -167,7 +175,7 @@ blastoff() { # load a url | |||
167 | url="$(run transform_resource "$BOLLUX_URL" "$1")" | 175 | url="$(run transform_resource "$BOLLUX_URL" "$1")" |
168 | fi | 176 | fi |
169 | [[ "$url" != *://* ]] && url="$BOLLUX_PROTO://$url" | 177 | [[ "$url" != *://* ]] && url="$BOLLUX_PROTO://$url" |
170 | url="$(trim "$url")" | 178 | url="$(trim_string "$url")" |
171 | proto="${url%://*}" | 179 | proto="${url%://*}" |
172 | 180 | ||
173 | log d "PROTO='$proto' URL='$url'" | 181 | log d "PROTO='$proto' URL='$url'" |
@@ -190,6 +198,7 @@ blastoff() { # load a url | |||
190 | } | 198 | } |
191 | } | 199 | } |
192 | 200 | ||
201 | # transform a URI according to RFC 3986 sec 5.2.2 | ||
193 | transform_resource() { # transform_resource BASE_URL REFERENCE_URL | 202 | transform_resource() { # transform_resource BASE_URL REFERENCE_URL |
194 | local -A R B T # reference, base url, target | 203 | local -A R B T # reference, base url, target |
195 | eval "$(run parse_url B "$1")" | 204 | eval "$(run parse_url B "$1")" |
@@ -246,7 +255,8 @@ transform_resource() { # transform_resource BASE_URL REFERENCE_URL | |||
246 | printf '%s\n' "$r" | 255 | printf '%s\n' "$r" |
247 | } | 256 | } |
248 | 257 | ||
249 | merge_paths() { # 5.2.3 | 258 | # merge URL paths according to RFC 3986 sec 5.2.3 |
259 | merge_paths() { # merge_paths BASE_AUTHORITY BASE_PATH REFERENCE_PATH | ||
250 | # shellcheck disable=2034 | 260 | # shellcheck disable=2034 |
251 | local B_authority="$1" | 261 | local B_authority="$1" |
252 | local B_path="$2" | 262 | local B_path="$2" |
@@ -269,7 +279,8 @@ merge_paths() { # 5.2.3 | |||
269 | fi | 279 | fi |
270 | } | 280 | } |
271 | 281 | ||
272 | remove_dot_segments() { # 5.2.4 | 282 | # remove dot segments in paths according to RFC 3986 sec 5.2.4 |
283 | remove_dot_segments() { # remove_dot_segments PATH | ||
273 | local input="$1" | 284 | local input="$1" |
274 | local output | 285 | local output |
275 | while [[ "$input" ]]; do | 286 | while [[ "$input" ]]; do |
@@ -292,13 +303,12 @@ remove_dot_segments() { # 5.2.4 | |||
292 | printf '%s\n' "${output//\/\//\//}" | 303 | printf '%s\n' "${output//\/\//\//}" |
293 | } | 304 | } |
294 | 305 | ||
306 | # parse a url using the reference regex in RFC 3986 appendix B | ||
295 | parse_url() { # eval "$(split_url NAME STRING)" => NAME[...] | 307 | parse_url() { # eval "$(split_url NAME STRING)" => NAME[...] |
296 | local name="$1" | 308 | local name="$1" |
297 | local string="$2" | 309 | local string="$2" |
298 | # shopt -u extglob # TODO port re ^ to extglob syntax | ||
299 | local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' | 310 | local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' |
300 | [[ $string =~ $re ]] || return $? | 311 | [[ $string =~ $re ]] || return $? |
301 | # shopt -s extglob | ||
302 | 312 | ||
303 | local scheme="${BASH_REMATCH[2]}" | 313 | local scheme="${BASH_REMATCH[2]}" |
304 | local authority="${BASH_REMATCH[4]}" | 314 | local authority="${BASH_REMATCH[4]}" |
@@ -316,18 +326,37 @@ parse_url() { # eval "$(split_url NAME STRING)" => NAME[...] | |||
316 | 326 | ||
317 | # is a NAME defined ('set' in bash)? | 327 | # is a NAME defined ('set' in bash)? |
318 | isdefined() { [[ "${!1+x}" ]]; } # isdefined NAME | 328 | isdefined() { [[ "${!1+x}" ]]; } # isdefined NAME |
329 | |||
319 | # is a NAME defined AND empty? | 330 | # is a NAME defined AND empty? |
320 | isempty() { [[ ! "${!1-x}" ]]; } # isempty NAME | 331 | isempty() { [[ ! "${!1-x}" ]]; } # isempty NAME |
321 | # split a string -- see pure bash bible | 332 | |
322 | split() { # split STRING DELIMITER | 333 | # work with URLs |
323 | local -a arr | 334 | # https://github.com/dylanaraps/pure-bash-bible/ |
324 | IFS=$'\n' read -d "" -ra arr <<<"${1//$2/$'\n'}" | 335 | urlencode() { # urlencode STRING |
325 | printf '%s\n' "${arr[@]}" | 336 | local LC_ALL=C |
337 | for ((i = 0; i < ${#1}; i++)); do | ||
338 | : "${1:i:1}" | ||
339 | case "$_" in | ||
340 | [a-zA-Z0-9.~_-]) | ||
341 | printf '%s' "$_" | ||
342 | ;; | ||
343 | *) | ||
344 | printf '%%%02X' "'$_" | ||
345 | ;; | ||
346 | esac | ||
347 | done | ||
348 | printf '\n' | ||
349 | } | ||
350 | |||
351 | # https://github.com/dylanaraps/pure-bash-bible/ | ||
352 | urldecode() { # urldecode STRING | ||
353 | : "${1//+/ }" | ||
354 | printf '%b\n' "${_//%/\\x}" | ||
326 | } | 355 | } |
327 | 356 | ||
328 | # GEMINI | 357 | # GEMINI |
329 | # https://gemini.circumlunar.space/docs/spec-spec.txt | 358 | # https://gemini.circumlunar.space/docs/specification.html |
330 | gemini_request() { | 359 | gemini_request() { # gemini_request URL |
331 | local url port server | 360 | local url port server |
332 | local ssl_cmd | 361 | local ssl_cmd |
333 | url="$1" | 362 | url="$1" |
@@ -342,7 +371,7 @@ gemini_request() { | |||
342 | run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null | 371 | run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null |
343 | } | 372 | } |
344 | 373 | ||
345 | gemini_response() { | 374 | gemini_response() { # gemini_response URL |
346 | local url code meta | 375 | local url code meta |
347 | local title | 376 | local title |
348 | url="$1" | 377 | url="$1" |
@@ -413,7 +442,7 @@ gemini_response() { | |||
413 | # GOPHER | 442 | # GOPHER |
414 | # https://tools.ietf.org/html/rfc1436 protocol | 443 | # https://tools.ietf.org/html/rfc1436 protocol |
415 | # https://tools.ietf.org/html/rfc4266 url | 444 | # https://tools.ietf.org/html/rfc4266 url |
416 | gopher_request() { | 445 | gopher_request() { # gopher_request URL |
417 | local url server port type path | 446 | local url server port type path |
418 | url="$1" | 447 | url="$1" |
419 | port=70 | 448 | port=70 |
@@ -432,7 +461,7 @@ gopher_request() { | |||
432 | passthru <&9 | 461 | passthru <&9 |
433 | } | 462 | } |
434 | 463 | ||
435 | gopher_response() { | 464 | gopher_response() { # gopher_response URL |
436 | local url pre type cur_server | 465 | local url pre type cur_server |
437 | pre=false | 466 | pre=false |
438 | url="$1" | 467 | url="$1" |
@@ -469,12 +498,14 @@ gopher_response() { | |||
469 | esac | 498 | esac |
470 | } | 499 | } |
471 | 500 | ||
501 | # 'cat' but in pure bash | ||
472 | passthru() { | 502 | passthru() { |
473 | while IFS= read -r; do | 503 | while IFS= read -r; do |
474 | printf '%s\n' "$REPLY" | 504 | printf '%s\n' "$REPLY" |
475 | done | 505 | done |
476 | } | 506 | } |
477 | 507 | ||
508 | # convert gophermap to text/gemini (probably naive) | ||
478 | gopher_convert() { | 509 | gopher_convert() { |
479 | local type label path server port regex | 510 | local type label path server port regex |
480 | # cf. https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk | 511 | # cf. https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk |
@@ -546,6 +577,7 @@ gopher_convert() { | |||
546 | exec 9>&- | 577 | exec 9>&- |
547 | } | 578 | } |
548 | 579 | ||
580 | # display the fetched content | ||
549 | display() { # display METADATA [TITLE] | 581 | display() { # display METADATA [TITLE] |
550 | local -a less_cmd | 582 | local -a less_cmd |
551 | local i mime charset | 583 | local i mime charset |
@@ -558,7 +590,7 @@ display() { # display METADATA [TITLE] | |||
558 | title="$2" | 590 | title="$2" |
559 | fi | 591 | fi |
560 | 592 | ||
561 | mime="$(trim "${hdr[0],,}")" | 593 | mime="$(trim_string "${hdr[0],,}")" |
562 | for ((i = 1; i <= "${#hdr[@]}"; i++)); do | 594 | for ((i = 1; i <= "${#hdr[@]}"; i++)); do |
563 | h="${hdr[$i]}" | 595 | h="${hdr[$i]}" |
564 | case "$h" in | 596 | case "$h" in |
@@ -601,7 +633,8 @@ display() { # display METADATA [TITLE] | |||
601 | esac | 633 | esac |
602 | } | 634 | } |
603 | 635 | ||
604 | less_prompt_escape() { | 636 | # escape strings for the less prompt |
637 | less_prompt_escape() { # less_prompt_escape STRING | ||
605 | local i | 638 | local i |
606 | for ((i = 0; i < ${#1}; i++)); do | 639 | for ((i = 0; i < ${#1}; i++)); do |
607 | : "${1:i:1}" | 640 | : "${1:i:1}" |
@@ -613,7 +646,8 @@ less_prompt_escape() { | |||
613 | printf '\n' | 646 | printf '\n' |
614 | } | 647 | } |
615 | 648 | ||
616 | mklesskey() { | 649 | # generate a lesskey(1) file for custom keybinds |
650 | mklesskey() { # mklesskey FILENAME | ||
617 | lesskey -o "$1" - <<-END | 651 | lesskey -o "$1" - <<-END |
618 | #command | 652 | #command |
619 | o quit 0 # 48 open a link | 653 | o quit 0 # 48 open a link |
@@ -631,14 +665,17 @@ mklesskey() { | |||
631 | END | 665 | END |
632 | } | 666 | } |
633 | 667 | ||
668 | # normalize files | ||
634 | normalize() { | 669 | normalize() { |
635 | shopt -s extglob | 670 | shopt -s extglob |
636 | while IFS= read -r; do | 671 | while IFS= read -r; do |
672 | # normalize line endings | ||
637 | printf '%s\n' "${REPLY//$'\r'?($'\n')/}" | 673 | printf '%s\n' "${REPLY//$'\r'?($'\n')/}" |
638 | done | 674 | done |
639 | shopt -u extglob | 675 | shopt -u extglob |
640 | } | 676 | } |
641 | 677 | ||
678 | # typeset a text/gemini document | ||
642 | typeset_gemini() { | 679 | typeset_gemini() { |
643 | local pre=false | 680 | local pre=false |
644 | local ln=0 # link number | 681 | local ln=0 # link number |
@@ -752,6 +789,7 @@ gemini_pre() { | |||
752 | printf "\e[${C_PRE}m%s${C_RESET}\n" "$1" | 789 | printf "\e[${C_PRE}m%s${C_RESET}\n" "$1" |
753 | } | 790 | } |
754 | 791 | ||
792 | # wrap lines on words to WIDTH | ||
755 | fold_line() { # fold_line WIDTH TEXT | 793 | fold_line() { # fold_line WIDTH TEXT |
756 | local width="$1" | 794 | local width="$1" |
757 | local margin="${2%%[![:space:]]*}" | 795 | local margin="${2%%[![:space:]]*}" |
@@ -778,7 +816,8 @@ fold_line() { # fold_line WIDTH TEXT | |||
778 | printf '\n' | 816 | printf '\n' |
779 | } | 817 | } |
780 | 818 | ||
781 | handle_keypress() { | 819 | # use the exit code from less (see mklesskey) to do things |
820 | handle_keypress() { # handle_keypress CODE | ||
782 | case "$1" in | 821 | case "$1" in |
783 | 48) # o - open a link -- show a menu of links on the page | 822 | 48) # o - open a link -- show a menu of links on the page |
784 | run select_url "$BOLLUX_PAGESRC" | 823 | run select_url "$BOLLUX_PAGESRC" |
@@ -812,7 +851,8 @@ handle_keypress() { | |||
812 | esac | 851 | esac |
813 | } | 852 | } |
814 | 853 | ||
815 | select_url() { | 854 | # select a URL from a text/gemini file |
855 | select_url() { # select_url FILE | ||
816 | run mapfile -t < <(extract_links <"$1") | 856 | run mapfile -t < <(extract_links <"$1") |
817 | PS3="OPEN> " | 857 | PS3="OPEN> " |
818 | select u in "${MAPFILE[@]}"; do | 858 | select u in "${MAPFILE[@]}"; do |
@@ -824,6 +864,7 @@ select_url() { | |||
824 | done </dev/tty | 864 | done </dev/tty |
825 | } | 865 | } |
826 | 866 | ||
867 | # extract the links from a text/gemini file | ||
827 | extract_links() { | 868 | extract_links() { |
828 | local url alt | 869 | local url alt |
829 | while read -r; do | 870 | while read -r; do |
@@ -840,6 +881,7 @@ extract_links() { | |||
840 | done | 881 | done |
841 | } | 882 | } |
842 | 883 | ||
884 | # download $BOLLUX_URL | ||
843 | download() { | 885 | download() { |
844 | tn="$(mktemp)" | 886 | tn="$(mktemp)" |
845 | log x "Downloading: '$BOLLUX_URL' => '$tn'..." | 887 | log x "Downloading: '$BOLLUX_URL' => '$tn'..." |
@@ -854,6 +896,7 @@ download() { | |||
854 | fi | 896 | fi |
855 | } | 897 | } |
856 | 898 | ||
899 | # initialize bollux | ||
857 | bollux_init() { | 900 | bollux_init() { |
858 | # Trap cleanup | 901 | # Trap cleanup |
859 | trap bollux_cleanup INT QUIT EXIT | 902 | trap bollux_cleanup INT QUIT EXIT |
@@ -866,12 +909,14 @@ bollux_init() { | |||
866 | run mkdir -p "${BOLLUX_HISTFILE%/*}" | 909 | run mkdir -p "${BOLLUX_HISTFILE%/*}" |
867 | } | 910 | } |
868 | 911 | ||
912 | # clean up on exit | ||
869 | bollux_cleanup() { | 913 | bollux_cleanup() { |
870 | # Stubbed in case of need in future | 914 | # Stubbed in case of need in future |
871 | : | 915 | : |
872 | } | 916 | } |
873 | 917 | ||
874 | history_append() { # history_append url TITLE | 918 | # append a URL to history |
919 | history_append() { # history_append URL TITLE | ||
875 | BOLLUX_URL="$1" | 920 | BOLLUX_URL="$1" |
876 | # date/time, url, title (best guess) | 921 | # date/time, url, title (best guess) |
877 | run printf '%(%FT%T)T\t%s\t%s\n' -1 "$1" "$2" >>"$BOLLUX_HISTFILE" | 922 | run printf '%(%FT%T)T\t%s\t%s\n' -1 "$1" "$2" >>"$BOLLUX_HISTFILE" |
@@ -879,6 +924,7 @@ history_append() { # history_append url TITLE | |||
879 | ((HN += 1)) | 924 | ((HN += 1)) |
880 | } | 925 | } |
881 | 926 | ||
927 | # move back in history (session) | ||
882 | history_back() { | 928 | history_back() { |
883 | log d "HN=$HN" | 929 | log d "HN=$HN" |
884 | ((HN -= 2)) | 930 | ((HN -= 2)) |
@@ -890,6 +936,7 @@ history_back() { | |||
890 | run blastoff "${HISTORY[$HN]}" | 936 | run blastoff "${HISTORY[$HN]}" |
891 | } | 937 | } |
892 | 938 | ||
939 | # move forward in history (session) | ||
893 | history_forward() { | 940 | history_forward() { |
894 | log d "HN=$HN" | 941 | log d "HN=$HN" |
895 | if ((HN >= ${#HISTORY[@]})); then | 942 | if ((HN >= ${#HISTORY[@]})); then |