From 903d1c18860c44deaf72b09dd24837fe524a7f28 Mon Sep 17 00:00:00 2001 From: Case Duckworth Date: Thu, 4 Mar 2021 13:00:16 -0600 Subject: [PATCH] Move functions around for better sense and document more --- bollux | 448 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 232 insertions(+), 216 deletions(-) diff --git a/bollux b/bollux index 2de37ab..d51f444 100755 --- a/bollux +++ b/bollux @@ -62,139 +62,13 @@ usage: flags: -h show this help and exit -q be quiet: log no messages - -v verbose: log more messages + -v be verbose: log more messages parameters: URL the URL to start in If not provided, the user will be prompted. END } -# UTILITY FUNCTIONS ############################################################ - -# Run a command, but log it first. -# -# See `log' for the available levels. -run() { # run COMMAND... - # I have to add a `trap' here for SIGINT to work properly. - trap bollux_quit SIGINT - log debug "$*" - "$@" -} - -# Exit with an error and a message describing it. -die() { # die EXIT_CODE MESSAGE - local ec="$1" - shift - log error "$*" - exit "$ec" -} - -# Exit with success, printing a fun message. -# -# The default message is from the wonderful show "Cowboy Bebop." -bollux_quit() { - printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG" - exit -} -# SIGINT is C-c, and I want to make sure bollux quits when it's typed. -trap bollux_quit SIGINT - -# Trim leading and trailing whitespace from a string. -# -# [1]: #trim-leading-and-trailing-white-space-from-string -trim_string() { # trim_string STRING - : "${1#"${1%%[![:space:]]*}"}" - : "${_%"${_##*[![:space:]]}"}" - printf '%s\n' "$_" -} - -# Cycle a variable. -# -# e.g. 'cycle_list one,two,three' => 'two,three,one' -cycle_list() { # cycle_list LIST DELIM - local list="${!1}" delim="$2" - local first="${list%%${delim}*}" - local rest="${list#*${delim}}" - printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}" -} - -# Determine the first element of a delimited list. -# -# e.g. 'first one,two,three' => 'one' -first() { # first LIST DELIM - local list="${!1}" delim="$2" - printf '%s\n' "${list%%${delim}*}" -} - -# Log a message to stderr (&2). -# -# TODO: document -log() { # log LEVEL MESSAGE - [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return - local fmt - - case "$1" in - ([dD]*) # debug - [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return - fmt=34 - ;; - ([eE]*) # error - fmt=31 - ;; - (*) fmt=1 ;; - esac - shift - - printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*" -} - -# Set the terminal title. -set_title() { # set_title STRING - printf '\e]2;%s\007' "$*" -} - -# Prompt the user for input. -# -# This is a thin wrapper around `read', a bash built-in. Because of the -# way bollux messes around with stein and stdout, I need to read directly from -# the TTY with this function. -prompt() { # prompt [-u] PROMPT [READ_ARGS...] - local read_cmd=(read -e -r) - if [[ "$1" == "-u" ]]; then - read_cmd+=(-i "$BOLLUX_URL") - shift - fi - local prompt="$1" - shift - read_cmd+=(-p "$prompt> ") - "${read_cmd[@]}" <(:) || : -} - -# MAIN BOLLUX DISPATCH FUNCTIONS ############################################### - # Main entry point into `bollux'. # # See the `if' block at the bottom of this script. @@ -251,10 +125,15 @@ bollux_config() { if [ -f "$BOLLUX_CONFIG" ]; then log debug "Loading config file '$BOLLUX_CONFIG'" + # Shellcheck gets mad when we try to source a file behind a + # variable -- it doesn't know where it is. This line ignores + # that warning, since the user can put $BOLLUX_CONFIG wherever. # shellcheck disable=1090 . "$BOLLUX_CONFIG" else - log debug "Can't load config file '$BOLLUX_CONFIG'." + # It's an error if bollux can't find the config file, but I + # don't want to kill the program over it. + log error "Can't load config file '$BOLLUX_CONFIG'." fi ## behavior @@ -301,67 +180,173 @@ bollux_config() { UC_BLANK=':?:' # internal use only, should be non-URL chars } -# Load a URL. +# Initialize bollux state +bollux_init() { + # Trap cleanup + trap bollux_cleanup INT QUIT EXIT + # State + REDIRECTS=0 + set -f + # History + declare -a HISTORY # history is kept in an array + HN=0 # position of history in the array + run mkdir -p "${BOLLUX_HISTFILE%/*}" + # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds) + run rm -f "$BOLLUX_LESSKEY" + mklesskey +} + +# Cleanup on exit +bollux_cleanup() { + # Stubbed in case of need in future + : +} + +# Exit with success, printing a fun message. # -# I was feeling fancy when I named this function -- a more descriptive name -# would be 'bollux_goto' or something. -blastoff() { # blastoff [-u] URL - local u +# The default message is from the wonderful show "Cowboy Bebop." +bollux_quit() { + bollux_cleanup + printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG" + exit +} +# SIGINT is C-c, and I want to make sure bollux quits when it's typed. +trap bollux_quit SIGINT - # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with - # a protocol string and no extraneous whitespace. Since bollux can't - # trust the user to input a proper URL at a prompt, nor capsule authors - # to fully-form their URLs, so the -u flag is necessary for those - # use-cases. Otherwise, bollux knows the URL is well-formed -- or - # should be, due to the Gemini specification. +# UTILITY FUNCTIONS ############################################################ + +# Run a command, but log it first. +# +# See `log' for the available levels. +run() { # run COMMAND... + # I have to add a `trap' here for SIGINT to work properly. + trap bollux_quit SIGINT + log debug "$*" + "$@" +} + + +# Log a message to stderr (&2). +# +# `log' in this script can take 3 different parameters: `d', `e', and `x', where +# `x' is any other string (though I usually use `x'), followed by the message to +# log. Most messages are either `d' (debug) level or `x' (diagnostic) level, +# meaning I want to show them all the time or only when bollux is called with +# `-v' (verbose). The levels are somewhat arbitrary, like I suspect all logging +# levels are, but you can read the rest of bollux to see what I've chosen to +# classify as what. +log() { # log LEVEL MESSAGE... + # 'QUIET' means don't log anything. + [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return + local fmt # ANSI escape code + + case "$1" in + ([dD]*) # Debug level -- only print if bollux -v. + [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return + fmt=34 # Blue + ;; + ([eE]*) # Error level -- always print. + fmt=31 # Red + ;; + (*) # Diagnostic level -- print unless QUIET. + fmt=1 # Bold + ;; + esac + shift + + printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' \ + "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*" +} + +# Exit with an error and a message describing it. +die() { # die EXIT_CODE MESSAGE + local exit_code="$1" + shift + log error "$*" + exit "$exit_code" +} + +# Trim leading and trailing whitespace from a string. +# +# [1]: #trim-leading-and-trailing-white-space-from-string +trim_string() { # trim_string STRING + : "${1#"${1%%[![:space:]]*}"}" + : "${_%"${_##*[![:space:]]}"}" + printf '%s\n' "$_" +} + +# Cycle a variable in a list given a delimiter. +# +# e.g. 'list_cycle one,two,three ,' => 'two,three,one' +list_cycle() { # list_cycle LIST DELIM + # I could've set up `list_cycle' to use an array instead of a delimited + # string, but the one variable this function is used for is + # T_PRE_DISPLAY, which is user-configurable. I wanted it to be as easy + # to configure for users who might not immediately know the bash array + # syntax, but can figure out 'variable=value' without much thought. + local list="${!1}" # Pass the list by name, not value + local delim="$2" # The delimiter of the string + local first="${list%%${delim}*}" # The first element + local rest="${list#*${delim}}" # The rest of the elements + # -v prints to the variable specified. + printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}" +} + +# Set the terminal title. +set_title() { # set_title TITLE... + printf '\e]2;%s\007' "$*" +} + +# Prompt the user for input. +# +# This is a thin wrapper around `read', a bash built-in. Because of the +# way bollux messes around with stdin and stdout, I need to read directly from +# the TTY with this function. +prompt() { # prompt [-u] PROMPT [READ_ARGS...] + # `-e' gets the line "interactively", so it can see history and stuff + # `-r' reads a "raw" string, i.e., without backslash escaping + local read_cmd=(read -e -r) if [[ "$1" == "-u" ]]; then - u="$(run uwellform "$2")" - else - u="$1" + # `-i TEXT' uses TEXT as the initial text for `read' + read_cmd+=(-i "$BOLLUX_URL") + shift fi + local prompt="$1" # How to prompt the user + shift + read_cmd+=(-p "$prompt> ") + "${read_cmd[@]}" /dev/null 2>&1; then - run "${url[1]}_request" "$url" - else - die 99 "No request handler for '${url[1]}'" - fi - } | run normalize | { - if declare -F "${url[1]}_response" >/dev/null 2>&1; then - run "${url[1]}_response" "$url" - else - log d \ - "No response handler for '${url[1]}';" \ - " passing thru" - passthru - fi - } +# Bash built-in replacement for `sleep' +# +# The commentary for `passthru' applies here as well, though I didn't write this +# function -- Dylan Araps did. +# +# [1]: #use-read-as-an-alternative-to-the-sleep-command +sleep() { # sleep SECONDS + read -rt "$1" <> <(:) || : +} + +# Normalize files. +normalize() { + shopt -s extglob + while IFS= read -r; do + # Normalize line endings to Unix-style (LF) + printf '%s\n' "${REPLY//$'\r'?($'\n')/}" + done + shopt -u extglob } # URLS ######################################################################### @@ -1156,16 +1141,6 @@ END fi } -# normalize files -normalize() { - shopt -s extglob - while IFS= read -r; do - # normalize line endings - printf '%s\n' "${REPLY//$'\r'?($'\n')/}" - done - shopt -u extglob -} - # typeset a text/gemini document typeset_gemini() { local pre=false @@ -1411,7 +1386,7 @@ handle_keypress() { # handle_keypress CODE run blastoff -u "$REPLY" ;; (54) # ` - change alt-text visibility and refresh - run cycle_list T_PRE_DISPLAY , + run list_cycle T_PRE_DISPLAY , run blastoff "$BOLLUX_URL" ;; (55) # 55-57 -- still available for binding @@ -1472,28 +1447,6 @@ download() { fi } -# initialize bollux -bollux_init() { - # Trap cleanup - trap bollux_cleanup INT QUIT EXIT - # State - REDIRECTS=0 - set -f - # History - declare -a HISTORY # history is kept in an array - HN=0 # position of history in the array - run mkdir -p "${BOLLUX_HISTFILE%/*}" - # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds) - run rm -f "$BOLLUX_LESSKEY" - mklesskey -} - -# clean up on exit -bollux_cleanup() { - # Stubbed in case of need in future - : -} - # append a URL to history history_append() { # history_append URL TITLE BOLLUX_URL="$1" @@ -1526,6 +1479,69 @@ history_forward() { run blastoff "${HISTORY[$HN]}" } +# Load a URL. +# +# I was feeling fancy when I named this function -- a more descriptive name +# would be 'bollux_goto' or something. +blastoff() { # blastoff [-u] URL + local u + + # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with + # a protocol string and no extraneous whitespace. Since bollux can't + # trust the user to input a proper URL at a prompt, nor capsule authors + # to fully-form their URLs, so the -u flag is necessary for those + # use-cases. Otherwise, bollux knows the URL is well-formed -- or + # should be, due to the Gemini specification. + if [[ "$1" == "-u" ]]; then + u="$(run uwellform "$2")" + else + u="$1" + fi + + # After ensuring the URL is well-formed, `blastoff' needs to transform + # it according to the transform rules of RFC 3986 (see §5.2.2), which + # turns relative references into absolute references that bollux can use + # in its request to the server. That's followed by a check that the + # protocol is set, defaulting to Gemini if it isn't. + # + # Implementation detail: because Bash is really stupid when it comes to + # arrays, the URL functions u* (see below) work with an array defined + # with `local -a' and passed by name, not by value. Thus, the + # `urltransform url ...' instead of `urltransform "${url[@]}"' or + # similar. In addition, the `ucdef' and `ucset' functions take the name + # of the array element as parameters, not the element itself. + local -a url + run utransform url "$BOLLUX_URL" "$u" + if ! ucdef url[1]; then + run ucset url[1] "$BOLLUX_PROTO" + fi + + # To try and keep `bollux' as extensible as possible, I've written it + # only to expect two functions for every protocol it supports: + # `x_request' and `x_response', where `x' is the name of the protocol + # (the first element of the built `url' array). `declare -F' looks only + # for functions in the current scope, failing if it doesn't exist. + # + # In between `x_request' and `x_response', `blastoff' normalizes the + # line endings to UNIX-style (LF) for ease of display. + { + if declare -F "${url[1]}_request" >/dev/null 2>&1; then + run "${url[1]}_request" "$url" + else + die 99 "No request handler for '${url[1]}'" + fi + } | run normalize | { + if declare -F "${url[1]}_response" >/dev/null 2>&1; then + run "${url[1]}_response" "$url" + else + log d \ + "No response handler for '${url[1]}';" \ + " passing thru" + passthru + fi + } +} + if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then ${DEBUG:-false} && set -x run bollux "$@"