about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--bollux229
1 files changed, 178 insertions, 51 deletions
diff --git a/bollux b/bollux index 26dc316..a7c5f93 100644 --- a/bollux +++ b/bollux
@@ -3,11 +3,45 @@
3# Author: Case Duckworth 3# Author: Case Duckworth
4# License: MIT 4# License: MIT
5# Version: 0.4.0 5# Version: 0.4.0
6#
7# Commentary:
8#
9# The impetus for this program came from a Mastodon conversation I had where
10# someone mentioned the "simplest possible Gemini client" was this:
11#
12# openssl s_client -gin_foe -quiet -connect $server:1965 <<< "$url"
13#
14# That's still at the heart of this program (see `gemini_request'): `bollux' is
15# basically a half-functioning convenience wrapper around that openssl call.
16# The first versions of `bollux' used `gawk' and a lot of other tools on top of
17# bash, but after reading Dylan Araps' Pure Bash Bible[1] and other works, I
18# decided to make as much of it in Bash as possible. Thus, currently `bollux'
19# requires `bash' v. 4+, `less' (a recent, non-busybox version), `dd' for
20# downloads, `openssl' for requests, and `iconv' to convert pages to UTF-8.
21# Future versions will hopefully have a pager fully implemented in bash, so that
22# I won't have to worry about less's weird incompatibilities and keybinding
23# things. That's a major project though, and I'm scared.
24#
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
27# code" that I'll use to reference them in those comments, if necessary to keep
28# them shorter than 80 characters.
29#
30# [1]: https://github.com/dylanaraps/pure-bash-bible [PBB]
31# [2]: https://tools.ietf.org/html/rfc3986 [URLspec]
32# [3]: https://gemini.circumlunar.space/docs/specification.html [GEMspec]
33# [4]: https://tools.ietf.org/html/rfc1436 [GOPHERprotocol]
34# [5]: https://tools.ietf.org/html/rfc4266 [GOPHERurl]
35# [6]: [GOPHER_GEMINI]:
36# https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk
37#
38# Code:
6 39
7# Program information 40# Program information
8PRGN="${0##*/}" # Easiest way to get the script name 41PRGN="${0##*/}" # Easiest way to get the script name
9VRSN=0.4.1 # I /try/ to follow semver? IDK. 42VRSN=0.4.1 # I /try/ to follow semver? IDK.
10 43
44# Print a useful help message (`bollux -h').
11bollux_usage() { 45bollux_usage() {
12 cat <<END 46 cat <<END
13$PRGN (v. $VRSN): a bash gemini client 47$PRGN (v. $VRSN): a bash gemini client
@@ -24,12 +58,19 @@ parameters:
24END 58END
25} 59}
26 60
61# UTILITY FUNCTIONS ############################################################
62
63# Run a command, but log it first.
64#
65# See `log' for the available levels.
27run() { # run COMMAND... 66run() { # run COMMAND...
67 # I have to add a `trap' here for SIGINT (C-c) to work properly.
28 trap bollux_quit SIGINT 68 trap bollux_quit SIGINT
29 log debug "$*" 69 log debug "$*"
30 "$@" 70 "$@"
31} 71}
32 72
73# Exit with an error and a message describing it.
33die() { # die EXIT_CODE MESSAGE 74die() { # die EXIT_CODE MESSAGE
34 local ec="$1" 75 local ec="$1"
35 shift 76 shift
@@ -37,20 +78,35 @@ die() { # die EXIT_CODE MESSAGE
37 exit "$ec" 78 exit "$ec"
38} 79}
39 80
40# builtin replacement for `sleep` 81# Exit with success, printing a fun message.
41# https://github.com/dylanaraps/pure-bash-bible#use-read-as-an-alternative-to-the-sleep-command 82#
83# The default message is from the wonderful show "Cowboy Bebop."
84bollux_quit() {
85 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
86 exit
87}
88# SIGINT is C-c, and I want to make sure bollux quits when it's typed.
89trap bollux_quit SIGINT
90
91# Bash built-in replacement for `sleep`
92#
93# PBB: #use-read-as-an-alternative-to-the-sleep-command
42sleep() { # sleep SECONDS 94sleep() { # sleep SECONDS
43 read -rt "$1" <> <(:) || : 95 read -rt "$1" <> <(:) || :
44} 96}
45 97
46# https://github.com/dylanaraps/pure-bash-bible/ 98# Trim leading and trailing whitespace from a string.
99#
100# PBB: #trim-leading-and-trailing-white-space-from-string
47trim_string() { # trim_string STRING 101trim_string() { # trim_string STRING
48 : "${1#"${1%%[![:space:]]*}"}" 102 : "${1#"${1%%[![:space:]]*}"}"
49 : "${_%"${_##*[![:space:]]}"}" 103 : "${_%"${_##*[![:space:]]}"}"
50 printf '%s\n' "$_" 104 printf '%s\n' "$_"
51} 105}
52 106
53# cycle a variable, e.g. from 'one,two,three' => 'two,three,one' 107# Cycle a variable.
108#
109# e.g. 'cycle_list one,two,three' => 'two,three,one'
54cycle_list() { # cycle_list LIST DELIM 110cycle_list() { # cycle_list LIST DELIM
55 local list="${!1}" delim="$2" 111 local list="${!1}" delim="$2"
56 local first="${list%%${delim}*}" 112 local first="${list%%${delim}*}"
@@ -58,12 +114,17 @@ cycle_list() { # cycle_list LIST DELIM
58 printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}" 114 printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}"
59} 115}
60 116
61# determine the first element of a list, e.g. 'one,two,three' => 'one' 117# Determine the first element of a delimited list.
118#
119# e.g. 'first one,two,three' => 'one'
62first() { # first LIST DELIM 120first() { # first LIST DELIM
63 local list="${!1}" delim="$2" 121 local list="${!1}" delim="$2"
64 printf '%s\n' "${list%%${delim}*}" 122 printf '%s\n' "${list%%${delim}*}"
65} 123}
66 124
125# Log a message to stderr (&2).
126#
127# TODO: document
67log() { # log LEVEL MESSAGE 128log() { # log LEVEL MESSAGE
68 [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return 129 [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
69 local fmt 130 local fmt
@@ -83,22 +144,49 @@ log() { # log LEVEL MESSAGE
83 printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*" 144 printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*"
84} 145}
85 146
86# main entry point 147# Set the terminal title.
148set_title() { # set_title STRING
149 printf '\e]2;%s\007' "$*"
150}
151
152# Prompt the user for input.
153#
154# This is a thin wrapper around `read', a bash built-in. Because of the
155# way bollux messes around with stein and stdout, I need to read directly from
156# the TTY with this function.
157prompt() { # prompt [-u] PROMPT [READ_ARGS...]
158 local read_cmd=(read -e -r)
159 if [[ "$1" == "-u" ]]; then
160 read_cmd+=(-i "$BOLLUX_URL")
161 shift
162 fi
163 local prompt="$1"
164 shift
165 read_cmd+=(-p "$prompt> ")
166 "${read_cmd[@]}" </dev/tty "$@"
167}
168
169# MAIN BOLLUX DISPATCH FUNCTIONS ###############################################
170
171# Main entry point into `bollux'.
172#
173# See the `if' block at the bottom of this script.
87bollux() { 174bollux() {
88 run bollux_config # TODO: figure out better config method 175 run bollux_config # TODO: figure out better config method
89 run bollux_args "$@" # and argument parsing 176 run bollux_args "$@" # and argument parsing
90 run bollux_init 177 run bollux_init
91 178
179 # If the user hasn't configured a home page, $BOLLUX_URL will be blank.
180 # So, prompt the user where to go.
92 if [[ ! "${BOLLUX_URL:+x}" ]]; then 181 if [[ ! "${BOLLUX_URL:+x}" ]]; then
93 run prompt GO BOLLUX_URL 182 run prompt GO BOLLUX_URL
94 fi 183 fi
95
96 log d "BOLLUX_URL='$BOLLUX_URL'" 184 log d "BOLLUX_URL='$BOLLUX_URL'"
97 185
98 run blastoff -u "$BOLLUX_URL" 186 run blastoff -u "$BOLLUX_URL" # Visit the specified URL.
99} 187}
100 188
101# process command-line arguments 189# Process command-line arguments.
102bollux_args() { 190bollux_args() {
103 while getopts :hvq OPT; do 191 while getopts :hvq OPT; do
104 case "$OPT" in 192 case "$OPT" in
@@ -113,14 +201,26 @@ bollux_args() {
113 esac 201 esac
114 done 202 done
115 shift $((OPTIND - 1)) 203 shift $((OPTIND - 1))
204
205 # If there's a leftover argument, it's the URL to visit.
116 if (($# == 1)); then 206 if (($# == 1)); then
117 BOLLUX_URL="$1" 207 BOLLUX_URL="$1"
118 fi 208 fi
119} 209}
120 210
121# process config file and set variables 211# Source the configuration file and set remaining variables.
212#
213# Since `bollux_config' is loaded before `bollux_args', there's no way to
214# specify a configuration file from the command line. I run `bollux_args'
215# second so that command-line options (mostly $BOLLUX_URL) can supersede
216# config-file options, and I'm not sure how to rectify the situation.
217#
218# Anyway, the config file `bollux.conf' is just a bash file that's sourced in
219# this function. After that, I use a little bash trick to set all the remaining
220# variables to default values with `: "${VAR:=value}"'.
122bollux_config() { 221bollux_config() {
123 : "${BOLLUX_CONFIG:=${XDG_CONFIG_HOME:-$HOME/.config}/bollux/bollux.conf}" 222 : "${BOLLUX_CONF_DIR:=${XDG_CONFIG_HOME:-$HOME/.config}/bollux}"
223 : "${BOLLUX_CONFIG:=$BOLLUX_CONF_DIR/bollux.conf}"
124 224
125 if [ -f "$BOLLUX_CONFIG" ]; then 225 if [ -f "$BOLLUX_CONFIG" ]; then
126 log debug "Loading config file '$BOLLUX_CONFIG'" 226 log debug "Loading config file '$BOLLUX_CONFIG'"
@@ -145,7 +245,7 @@ bollux_config() {
145 : "${KEY_FORWARD:=']'}" # go forward in the history 245 : "${KEY_FORWARD:=']'}" # go forward in the history
146 : "${KEY_REFRESH:=r}" # refresh the page 246 : "${KEY_REFRESH:=r}" # refresh the page
147 : "${KEY_CYCLE_PRE:=p}" # cycle T_PRE_DISPLAY 247 : "${KEY_CYCLE_PRE:=p}" # cycle T_PRE_DISPLAY
148 : "${BOLLUX_CUSTOM_LESSKEY:=${XDG_CONFIG_HOME:-$HOME/.config}/bollux/bollux.lesskey}" 248 : "${BOLLUX_CUSTOM_LESSKEY:=$BOLLUX_CONF_DIR/bollux.lesskey}"
149 ## files 249 ## files
150 : "${BOLLUX_DATADIR:=${XDG_DATA_HOME:-$HOME/.local/share}/bollux}" 250 : "${BOLLUX_DATADIR:=${XDG_DATA_HOME:-$HOME/.local/share}/bollux}"
151 : "${BOLLUX_DOWNDIR:=.}" # where to save downloads 251 : "${BOLLUX_DOWNDIR:=.}" # where to save downloads
@@ -154,7 +254,8 @@ bollux_config() {
154 BOLLUX_HISTFILE="$BOLLUX_DATADIR/history" # where to save history 254 BOLLUX_HISTFILE="$BOLLUX_DATADIR/history" # where to save history
155 ## typesetting 255 ## typesetting
156 : "${T_MARGIN:=4}" # left and right margin 256 : "${T_MARGIN:=4}" # left and right margin
157 : "${T_WIDTH:=0}" # width of the viewport -- 0 = get term width 257 : "${T_WIDTH:=0}" # width of the view port
258 # 0 = get term width
158 : "${T_PRE_DISPLAY:=both,pre,alt}" # how to view PRE blocks 259 : "${T_PRE_DISPLAY:=both,pre,alt}" # how to view PRE blocks
159 # colors -- these will be wrapped in \e[ __ m 260 # colors -- these will be wrapped in \e[ __ m
160 C_RESET='\e[0m' # reset 261 C_RESET='\e[0m' # reset
@@ -169,59 +270,63 @@ bollux_config() {
169 : "${C_QUOTE:=3}" # quote formatting 270 : "${C_QUOTE:=3}" # quote formatting
170 : "${C_PRE:=0}" # preformatted text formatting 271 : "${C_PRE:=0}" # preformatted text formatting
171 ## state 272 ## state
172 UC_BLANK=':?:' 273 UC_BLANK=':?:' # internal use only, should be non-URL chars
173} 274}
174 275
175# quit happily
176bollux_quit() {
177 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
178 exit
179}
180# trap C-c
181trap bollux_quit SIGINT
182 276
183# set the terminal title 277# Load a URL.
184set_title() { # set_title STRING 278#
185 printf '\e]2;%s\007' "$*" 279# I was feeling fancy when I named this function -- a more descriptive name
186} 280# would be 'bollux_goto' or something.
187
188# prompt for input
189prompt() { # prompt [-u] PROMPT [READ_ARGS...]
190 local read_cmd=(read -e -r)
191 if [[ "$1" == "-u" ]]; then
192 read_cmd+=(-i "$BOLLUX_URL")
193 shift
194 fi
195 local prompt="$1"
196 shift
197 read_cmd+=(-p "$prompt> ")
198 "${read_cmd[@]}" </dev/tty "$@"
199}
200
201# load a URL
202blastoff() { # blastoff [-u] URL 281blastoff() { # blastoff [-u] URL
203 local u 282 local u
204 283
284 # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with
285 # a protocol string and no extraneous whitespace. Since bollux can't
286 # trust the user to input a proper URL at a prompt, nor capsule authors
287 # to fully-form their URLs, so the -u flag is necessary for those
288 # use-cases. Otherwise, bollux knows the URL is well-formed -- or
289 # should be, due to the Gemini specification.
205 if [[ "$1" == "-u" ]]; then 290 if [[ "$1" == "-u" ]]; then
206 u="$(run uwellform "$2")" 291 u="$(run uwellform "$2")"
207 else 292 else
208 u="$1" 293 u="$1"
209 fi 294 fi
210 295
296 # After ensuring the URL is well-formed, `blastoff' needs to transform
297 # it according to the transform rules of RFC 3986 (see ยง5.2.2), which
298 # turns relative references into absolute references that bollux can use
299 # in its request to the server. That's followed by a check that the
300 # protocol is set, defaulting to Gemini if it isn't.
301 #
302 # Implementation detail: because Bash is really stupid when it comes to
303 # arrays, the URL functions u* (see below) work with an array defined
304 # with `local -a' and passed by name, not by value. Thus, the
305 # `urltransform url ...' instead of `urltransform "${url[@]}"' or
306 # similar. In addition, the `ucdef' and `ucset' functions take the name
307 # of the array element as parameters, not the element itself.
211 local -a url 308 local -a url
212 run utransform url "$BOLLUX_URL" "$u" 309 run utransform url "$BOLLUX_URL" "$u"
213 if ! ucdef url[1]; then 310 if ! ucdef url[1]; then
214 run ucset url[1] "$BOLLUX_PROTO" 311 run ucset url[1] "$BOLLUX_PROTO"
215 fi 312 fi
216 313
314 # To try and keep `bollux' as extensible as possible, I've written it
315 # only to expect two functions for every protocol it supports:
316 # `x_request' and `x_response', where `x' is the name of the protocol
317 # (the first element of the built `url' array). `declare -F' looks only
318 # for functions in the current scope, failing if it doesn't exist.
319 #
320 # In between `x_request' and `x_response', `blastoff' normalizes the
321 # line endings to UNIX-style (LF) for ease of display.
217 { 322 {
218 if declare -Fp "${url[1]}_request" >/dev/null 2>&1; then 323 if declare -F "${url[1]}_request" >/dev/null 2>&1; then
219 run "${url[1]}_request" "$url" 324 run "${url[1]}_request" "$url"
220 else 325 else
221 die 99 "No request handler for '${url[1]}'" 326 die 99 "No request handler for '${url[1]}'"
222 fi 327 fi
223 } | run normalize | { 328 } | run normalize | {
224 if declare -Fp "${url[1]}_response" >/dev/null 2>&1; then 329 if declare -F "${url[1]}_response" >/dev/null 2>&1; then
225 run "${url[1]}_response" "$url" 330 run "${url[1]}_response" "$url"
226 else 331 else
227 log d \ 332 log d \
@@ -232,8 +337,23 @@ blastoff() { # blastoff [-u] URL
232 } 337 }
233} 338}
234 339
235# URLS 340# URLS: https://tools.ietf.org/html/rfc3986 ####################################
236## https://tools.ietf.org/html/rfc3986 341#
342# 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
344# function.
345#
346# In addition, most of these functions take arrays or array elements passed /by
347# name/, instead of /value/ -- i.e., instead of calling `usplit $url', call
348# `usplit url'. Passing values by name is necessary because of Bash's weird
349# array handling.
350#
351################################################################################
352
353# Make sure a URL is "well-formed:" add a default protocol if it's missing and
354# trim whitespace.
355#
356# Useful for URLs that were probably input by humans.
237uwellform() { 357uwellform() {
238 local u="$1" 358 local u="$1"
239 359
@@ -246,6 +366,13 @@ uwellform() {
246 printf '%s\n' "$u" 366 printf '%s\n' "$u"
247} 367}
248 368
369# Split a URL into its constituent parts, placing them all in the given array.
370#
371# 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
373# it, the function bails.
374#
375# `usplit' takes advantage ... [CONTINUE HERE]
249usplit() { # usplit NAME:ARRAY URL:STRING 376usplit() { # usplit NAME:ARRAY URL:STRING
250 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' 377 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
251 [[ $2 =~ $re ]] || return $? 378 [[ $2 =~ $re ]] || return $?
@@ -408,7 +535,7 @@ pmerge() {
408 fi 535 fi
409} 536}
410 537
411# https://github.com/dylanaraps/pure-bash-bible/ 538# PBB
412uencode() { # uencode URL:STRING 539uencode() { # uencode URL:STRING
413 local LC_ALL=C 540 local LC_ALL=C
414 for ((i = 0; i < ${#1}; i++)); do 541 for ((i = 0; i < ${#1}; i++)); do
@@ -425,7 +552,7 @@ uencode() { # uencode URL:STRING
425 printf '\n' 552 printf '\n'
426} 553}
427 554
428# https://github.com/dylanaraps/pure-bash-bible/ 555# PBB
429udecode() { # udecode URL:STRING 556udecode() { # udecode URL:STRING
430 : "${1//+/ }" 557 : "${1//+/ }"
431 printf '%b\n' "${_//%/\\x}" 558 printf '%b\n' "${_//%/\\x}"
@@ -598,7 +725,7 @@ passthru() {
598# convert gophermap to text/gemini (probably naive) 725# convert gophermap to text/gemini (probably naive)
599gopher_convert() { 726gopher_convert() {
600 local type label path server port regex 727 local type label path server port regex
601 # cf. https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk 728 # [GOPHER_GEMINI]
602 while IFS= read -r; do 729 while IFS= read -r; do
603 printf -v regex '(.)([^\t]*)(\t([^\t]*)\t([^\t]*)\t([^\t]*))?' 730 printf -v regex '(.)([^\t]*)(\t([^\t]*)\t([^\t]*)\t([^\t]*))?'
604 if [[ "$REPLY" =~ $regex ]]; then 731 if [[ "$REPLY" =~ $regex ]]; then
@@ -753,7 +880,9 @@ mklesskey() { # mklesskey
753 if [[ -f "$BOLLUX_CUSTOM_LESSKEY" ]]; then 880 if [[ -f "$BOLLUX_CUSTOM_LESSKEY" ]]; then
754 log d "Using custom lesskey: '$BOLLUX_CUSTOM_LESSKEY'" 881 log d "Using custom lesskey: '$BOLLUX_CUSTOM_LESSKEY'"
755 BOLLUX_LESSKEY="${BOLLUX_CUSTOM_LESSKEY}" 882 BOLLUX_LESSKEY="${BOLLUX_CUSTOM_LESSKEY}"
756 elif [[ ! -f "$BOLLUX_LESSKEY" ]]; then 883 elif [[ -f "$BOLLUX_LESSKEY" ]]; then
884 log d "Found lesskey: '$BOLLUX_LESSKEY'"
885 else
757 log d "Generating lesskey: '$BOLLUX_LESSKEY'" 886 log d "Generating lesskey: '$BOLLUX_LESSKEY'"
758 lesskey -o "$BOLLUX_LESSKEY" - <<END 887 lesskey -o "$BOLLUX_LESSKEY" - <<END
759#command 888#command
@@ -771,8 +900,6 @@ l right-scroll
771? status # 'status' will show a little help thing. 900? status # 'status' will show a little help thing.
772= noaction 901= noaction
773END 902END
774 else
775 log d "Found lesskey: '$BOLLUX_LESSKEY'"
776 fi 903 fi
777} 904}
778 905
@@ -1104,7 +1231,7 @@ bollux_init() {
1104 HN=0 # position of history in the array 1231 HN=0 # position of history in the array
1105 run mkdir -p "${BOLLUX_HISTFILE%/*}" 1232 run mkdir -p "${BOLLUX_HISTFILE%/*}"
1106 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds) 1233 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds)
1107 run rm "$BOLLUX_LESSKEY" 1234 run rm -f "$BOLLUX_LESSKEY"
1108 mklesskey 1235 mklesskey
1109} 1236}
1110 1237