about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbollux448
1 files changed, 232 insertions, 216 deletions
diff --git a/bollux b/bollux index 2de37ab..d51f444 100755 --- a/bollux +++ b/bollux
@@ -62,139 +62,13 @@ usage:
62flags: 62flags:
63 -h show this help and exit 63 -h show this help and exit
64 -q be quiet: log no messages 64 -q be quiet: log no messages
65 -v verbose: log more messages 65 -v be verbose: log more messages
66parameters: 66parameters:
67 URL the URL to start in 67 URL the URL to start in
68 If not provided, the user will be prompted. 68 If not provided, the user will be prompted.
69END 69END
70} 70}
71 71
72# UTILITY FUNCTIONS ############################################################
73
74# Run a command, but log it first.
75#
76# See `log' for the available levels.
77run() { # run COMMAND...
78 # I have to add a `trap' here for SIGINT to work properly.
79 trap bollux_quit SIGINT
80 log debug "$*"
81 "$@"
82}
83
84# Exit with an error and a message describing it.
85die() { # die EXIT_CODE MESSAGE
86 local ec="$1"
87 shift
88 log error "$*"
89 exit "$ec"
90}
91
92# Exit with success, printing a fun message.
93#
94# The default message is from the wonderful show "Cowboy Bebop."
95bollux_quit() {
96 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
97 exit
98}
99# SIGINT is C-c, and I want to make sure bollux quits when it's typed.
100trap bollux_quit SIGINT
101
102# Trim leading and trailing whitespace from a string.
103#
104# [1]: #trim-leading-and-trailing-white-space-from-string
105trim_string() { # trim_string STRING
106 : "${1#"${1%%[![:space:]]*}"}"
107 : "${_%"${_##*[![:space:]]}"}"
108 printf '%s\n' "$_"
109}
110
111# Cycle a variable.
112#
113# e.g. 'cycle_list one,two,three' => 'two,three,one'
114cycle_list() { # cycle_list LIST DELIM
115 local list="${!1}" delim="$2"
116 local first="${list%%${delim}*}"
117 local rest="${list#*${delim}}"
118 printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}"
119}
120
121# Determine the first element of a delimited list.
122#
123# e.g. 'first one,two,three' => 'one'
124first() { # first LIST DELIM
125 local list="${!1}" delim="$2"
126 printf '%s\n' "${list%%${delim}*}"
127}
128
129# Log a message to stderr (&2).
130#
131# TODO: document
132log() { # log LEVEL MESSAGE
133 [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
134 local fmt
135
136 case "$1" in
137 ([dD]*) # debug
138 [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return
139 fmt=34
140 ;;
141 ([eE]*) # error
142 fmt=31
143 ;;
144 (*) fmt=1 ;;
145 esac
146 shift
147
148 printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*"
149}
150
151# Set the terminal title.
152set_title() { # set_title STRING
153 printf '\e]2;%s\007' "$*"
154}
155
156# Prompt the user for input.
157#
158# This is a thin wrapper around `read', a bash built-in. Because of the
159# way bollux messes around with stein and stdout, I need to read directly from
160# the TTY with this function.
161prompt() { # prompt [-u] PROMPT [READ_ARGS...]
162 local read_cmd=(read -e -r)
163 if [[ "$1" == "-u" ]]; then
164 read_cmd+=(-i "$BOLLUX_URL")
165 shift
166 fi
167 local prompt="$1"
168 shift
169 read_cmd+=(-p "$prompt> ")
170 "${read_cmd[@]}" </dev/tty "$@"
171}
172
173
174# Bash built-in replacement for `cat'
175#
176# One of the more pedantic bits of bollux (is 'pedantic' the right word?) --
177# `cat' is more than likely installed on any system with bash, so this function
178# is really just here so I can say that bollux is written as purely in bash as
179# possible.
180passthru() {
181 while IFS= read -r; do
182 printf '%s\n' "$REPLY"
183 done
184}
185
186# Bash built-in replacement for `sleep'
187#
188# The commentary for `passthru' applies here as well, though I didn't write this
189# function -- Dylan Araps did.
190#
191# [1]: #use-read-as-an-alternative-to-the-sleep-command
192sleep() { # sleep SECONDS
193 read -rt "$1" <> <(:) || :
194}
195
196# MAIN BOLLUX DISPATCH FUNCTIONS ###############################################
197
198# Main entry point into `bollux'. 72# Main entry point into `bollux'.
199# 73#
200# See the `if' block at the bottom of this script. 74# See the `if' block at the bottom of this script.
@@ -251,10 +125,15 @@ bollux_config() {
251 125
252 if [ -f "$BOLLUX_CONFIG" ]; then 126 if [ -f "$BOLLUX_CONFIG" ]; then
253 log debug "Loading config file '$BOLLUX_CONFIG'" 127 log debug "Loading config file '$BOLLUX_CONFIG'"
128 # Shellcheck gets mad when we try to source a file behind a
129 # variable -- it doesn't know where it is. This line ignores
130 # that warning, since the user can put $BOLLUX_CONFIG wherever.
254 # shellcheck disable=1090 131 # shellcheck disable=1090
255 . "$BOLLUX_CONFIG" 132 . "$BOLLUX_CONFIG"
256 else 133 else
257 log debug "Can't load config file '$BOLLUX_CONFIG'." 134 # It's an error if bollux can't find the config file, but I
135 # don't want to kill the program over it.
136 log error "Can't load config file '$BOLLUX_CONFIG'."
258 fi 137 fi
259 138
260 ## behavior 139 ## behavior
@@ -301,67 +180,173 @@ bollux_config() {
301 UC_BLANK=':?:' # internal use only, should be non-URL chars 180 UC_BLANK=':?:' # internal use only, should be non-URL chars
302} 181}
303 182
304# Load a URL. 183# Initialize bollux state
184bollux_init() {
185 # Trap cleanup
186 trap bollux_cleanup INT QUIT EXIT
187 # State
188 REDIRECTS=0
189 set -f
190 # History
191 declare -a HISTORY # history is kept in an array
192 HN=0 # position of history in the array
193 run mkdir -p "${BOLLUX_HISTFILE%/*}"
194 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds)
195 run rm -f "$BOLLUX_LESSKEY"
196 mklesskey
197}
198
199# Cleanup on exit
200bollux_cleanup() {
201 # Stubbed in case of need in future
202 :
203}
204
205# Exit with success, printing a fun message.
305# 206#
306# I was feeling fancy when I named this function -- a more descriptive name 207# The default message is from the wonderful show "Cowboy Bebop."
307# would be 'bollux_goto' or something. 208bollux_quit() {
308blastoff() { # blastoff [-u] URL 209 bollux_cleanup
309 local u 210 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
211 exit
212}
213# SIGINT is C-c, and I want to make sure bollux quits when it's typed.
214trap bollux_quit SIGINT
310 215
311 # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with 216# UTILITY FUNCTIONS ############################################################
312 # a protocol string and no extraneous whitespace. Since bollux can't 217
313 # trust the user to input a proper URL at a prompt, nor capsule authors 218# Run a command, but log it first.
314 # to fully-form their URLs, so the -u flag is necessary for those 219#
315 # use-cases. Otherwise, bollux knows the URL is well-formed -- or 220# See `log' for the available levels.
316 # should be, due to the Gemini specification. 221run() { # run COMMAND...
222 # I have to add a `trap' here for SIGINT to work properly.
223 trap bollux_quit SIGINT
224 log debug "$*"
225 "$@"
226}
227
228
229# Log a message to stderr (&2).
230#
231# `log' in this script can take 3 different parameters: `d', `e', and `x', where
232# `x' is any other string (though I usually use `x'), followed by the message to
233# log. Most messages are either `d' (debug) level or `x' (diagnostic) level,
234# meaning I want to show them all the time or only when bollux is called with
235# `-v' (verbose). The levels are somewhat arbitrary, like I suspect all logging
236# levels are, but you can read the rest of bollux to see what I've chosen to
237# classify as what.
238log() { # log LEVEL MESSAGE...
239 # 'QUIET' means don't log anything.
240 [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
241 local fmt # ANSI escape code
242
243 case "$1" in
244 ([dD]*) # Debug level -- only print if bollux -v.
245 [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return
246 fmt=34 # Blue
247 ;;
248 ([eE]*) # Error level -- always print.
249 fmt=31 # Red
250 ;;
251 (*) # Diagnostic level -- print unless QUIET.
252 fmt=1 # Bold
253 ;;
254 esac
255 shift
256
257 printf >&2 '\e[%sm%s:%s:\e[0m\t%s\n' \
258 "$fmt" "$PRGN" "${FUNCNAME[1]}" "$*"
259}
260
261# Exit with an error and a message describing it.
262die() { # die EXIT_CODE MESSAGE
263 local exit_code="$1"
264 shift
265 log error "$*"
266 exit "$exit_code"
267}
268
269# Trim leading and trailing whitespace from a string.
270#
271# [1]: #trim-leading-and-trailing-white-space-from-string
272trim_string() { # trim_string STRING
273 : "${1#"${1%%[![:space:]]*}"}"
274 : "${_%"${_##*[![:space:]]}"}"
275 printf '%s\n' "$_"
276}
277
278# Cycle a variable in a list given a delimiter.
279#
280# e.g. 'list_cycle one,two,three ,' => 'two,three,one'
281list_cycle() { # list_cycle LIST<string> DELIM
282 # I could've set up `list_cycle' to use an array instead of a delimited
283 # string, but the one variable this function is used for is
284 # T_PRE_DISPLAY, which is user-configurable. I wanted it to be as easy
285 # to configure for users who might not immediately know the bash array
286 # syntax, but can figure out 'variable=value' without much thought.
287 local list="${!1}" # Pass the list by name, not value
288 local delim="$2" # The delimiter of the string
289 local first="${list%%${delim}*}" # The first element
290 local rest="${list#*${delim}}" # The rest of the elements
291 # -v prints to the variable specified.
292 printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}"
293}
294
295# Set the terminal title.
296set_title() { # set_title TITLE...
297 printf '\e]2;%s\007' "$*"
298}
299
300# Prompt the user for input.
301#
302# This is a thin wrapper around `read', a bash built-in. Because of the
303# way bollux messes around with stdin and stdout, I need to read directly from
304# the TTY with this function.
305prompt() { # prompt [-u] PROMPT [READ_ARGS...]
306 # `-e' gets the line "interactively", so it can see history and stuff
307 # `-r' reads a "raw" string, i.e., without backslash escaping
308 local read_cmd=(read -e -r)
317 if [[ "$1" == "-u" ]]; then 309 if [[ "$1" == "-u" ]]; then
318 u="$(run uwellform "$2")" 310 # `-i TEXT' uses TEXT as the initial text for `read'
319 else 311 read_cmd+=(-i "$BOLLUX_URL")
320 u="$1" 312 shift
321 fi 313 fi
314 local prompt="$1" # How to prompt the user
315 shift
316 read_cmd+=(-p "$prompt> ")
317 "${read_cmd[@]}" </dev/tty "$@"
318}
322 319
323 # After ensuring the URL is well-formed, `blastoff' needs to transform 320# Bash built-in replacement for `cat'
324 # it according to the transform rules of RFC 3986 (see §5.2.2), which 321#
325 # turns relative references into absolute references that bollux can use 322# One of the more pedantic bits of bollux (is 'pedantic' the right word?) --
326 # in its request to the server. That's followed by a check that the 323# `cat' is more than likely installed on any system with bash, so this function
327 # protocol is set, defaulting to Gemini if it isn't. 324# is really just here so I can say that bollux is written as purely in bash as
328 # 325# possible.
329 # Implementation detail: because Bash is really stupid when it comes to 326passthru() {
330 # arrays, the URL functions u* (see below) work with an array defined 327 while IFS= read -r; do
331 # with `local -a' and passed by name, not by value. Thus, the 328 printf '%s\n' "$REPLY"
332 # `urltransform url ...' instead of `urltransform "${url[@]}"' or 329 done
333 # similar. In addition, the `ucdef' and `ucset' functions take the name 330}
334 # of the array element as parameters, not the element itself.
335 local -a url
336 run utransform url "$BOLLUX_URL" "$u"
337 if ! ucdef url[1]; then
338 run ucset url[1] "$BOLLUX_PROTO"
339 fi
340 331
341 # To try and keep `bollux' as extensible as possible, I've written it 332# Bash built-in replacement for `sleep'
342 # only to expect two functions for every protocol it supports: 333#
343 # `x_request' and `x_response', where `x' is the name of the protocol 334# The commentary for `passthru' applies here as well, though I didn't write this
344 # (the first element of the built `url' array). `declare -F' looks only 335# function -- Dylan Araps did.
345 # for functions in the current scope, failing if it doesn't exist. 336#
346 # 337# [1]: #use-read-as-an-alternative-to-the-sleep-command
347 # In between `x_request' and `x_response', `blastoff' normalizes the 338sleep() { # sleep SECONDS
348 # line endings to UNIX-style (LF) for ease of display. 339 read -rt "$1" <> <(:) || :
349 { 340}
350 if declare -F "${url[1]}_request" >/dev/null 2>&1; then 341
351 run "${url[1]}_request" "$url" 342# Normalize files.
352 else 343normalize() {
353 die 99 "No request handler for '${url[1]}'" 344 shopt -s extglob
354 fi 345 while IFS= read -r; do
355 } | run normalize | { 346 # Normalize line endings to Unix-style (LF)
356 if declare -F "${url[1]}_response" >/dev/null 2>&1; then 347 printf '%s\n' "${REPLY//$'\r'?($'\n')/}"
357 run "${url[1]}_response" "$url" 348 done
358 else 349 shopt -u extglob
359 log d \
360 "No response handler for '${url[1]}';" \
361 " passing thru"
362 passthru
363 fi
364 }
365} 350}
366 351
367# URLS ######################################################################### 352# URLS #########################################################################
@@ -1156,16 +1141,6 @@ END
1156 fi 1141 fi
1157} 1142}
1158 1143
1159# normalize files
1160normalize() {
1161 shopt -s extglob
1162 while IFS= read -r; do
1163 # normalize line endings
1164 printf '%s\n' "${REPLY//$'\r'?($'\n')/}"
1165 done
1166 shopt -u extglob
1167}
1168
1169# typeset a text/gemini document 1144# typeset a text/gemini document
1170typeset_gemini() { 1145typeset_gemini() {
1171 local pre=false 1146 local pre=false
@@ -1411,7 +1386,7 @@ handle_keypress() { # handle_keypress CODE
1411 run blastoff -u "$REPLY" 1386 run blastoff -u "$REPLY"
1412 ;; 1387 ;;
1413 (54) # ` - change alt-text visibility and refresh 1388 (54) # ` - change alt-text visibility and refresh
1414 run cycle_list T_PRE_DISPLAY , 1389 run list_cycle T_PRE_DISPLAY ,
1415 run blastoff "$BOLLUX_URL" 1390 run blastoff "$BOLLUX_URL"
1416 ;; 1391 ;;
1417 (55) # 55-57 -- still available for binding 1392 (55) # 55-57 -- still available for binding
@@ -1472,28 +1447,6 @@ download() {
1472 fi 1447 fi
1473} 1448}
1474 1449
1475# initialize bollux
1476bollux_init() {
1477 # Trap cleanup
1478 trap bollux_cleanup INT QUIT EXIT
1479 # State
1480 REDIRECTS=0
1481 set -f
1482 # History
1483 declare -a HISTORY # history is kept in an array
1484 HN=0 # position of history in the array
1485 run mkdir -p "${BOLLUX_HISTFILE%/*}"
1486 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds)
1487 run rm -f "$BOLLUX_LESSKEY"
1488 mklesskey
1489}
1490
1491# clean up on exit
1492bollux_cleanup() {
1493 # Stubbed in case of need in future
1494 :
1495}
1496
1497# append a URL to history 1450# append a URL to history
1498history_append() { # history_append URL TITLE 1451history_append() { # history_append URL TITLE
1499 BOLLUX_URL="$1" 1452 BOLLUX_URL="$1"
@@ -1526,6 +1479,69 @@ history_forward() {
1526 run blastoff "${HISTORY[$HN]}" 1479 run blastoff "${HISTORY[$HN]}"
1527} 1480}
1528 1481
1482# Load a URL.
1483#
1484# I was feeling fancy when I named this function -- a more descriptive name
1485# would be 'bollux_goto' or something.
1486blastoff() { # blastoff [-u] URL
1487 local u
1488
1489 # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with
1490 # a protocol string and no extraneous whitespace. Since bollux can't
1491 # trust the user to input a proper URL at a prompt, nor capsule authors
1492 # to fully-form their URLs, so the -u flag is necessary for those
1493 # use-cases. Otherwise, bollux knows the URL is well-formed -- or
1494 # should be, due to the Gemini specification.
1495 if [[ "$1" == "-u" ]]; then
1496 u="$(run uwellform "$2")"
1497 else
1498 u="$1"
1499 fi
1500
1501 # 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
1503 # 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
1505 # protocol is set, defaulting to Gemini if it isn't.
1506 #
1507 # Implementation detail: because Bash is really stupid when it comes to
1508 # arrays, the URL functions u* (see below) work with an array defined
1509 # with `local -a' and passed by name, not by value. Thus, the
1510 # `urltransform url ...' instead of `urltransform "${url[@]}"' or
1511 # similar. In addition, the `ucdef' and `ucset' functions take the name
1512 # of the array element as parameters, not the element itself.
1513 local -a url
1514 run utransform url "$BOLLUX_URL" "$u"
1515 if ! ucdef url[1]; then
1516 run ucset url[1] "$BOLLUX_PROTO"
1517 fi
1518
1519 # To try and keep `bollux' as extensible as possible, I've written it
1520 # only to expect two functions for every protocol it supports:
1521 # `x_request' and `x_response', where `x' is the name of the protocol
1522 # (the first element of the built `url' array). `declare -F' looks only
1523 # for functions in the current scope, failing if it doesn't exist.
1524 #
1525 # In between `x_request' and `x_response', `blastoff' normalizes the
1526 # line endings to UNIX-style (LF) for ease of display.
1527 {
1528 if declare -F "${url[1]}_request" >/dev/null 2>&1; then
1529 run "${url[1]}_request" "$url"
1530 else
1531 die 99 "No request handler for '${url[1]}'"
1532 fi
1533 } | run normalize | {
1534 if declare -F "${url[1]}_response" >/dev/null 2>&1; then
1535 run "${url[1]}_response" "$url"
1536 else
1537 log d \
1538 "No response handler for '${url[1]}';" \
1539 " passing thru"
1540 passthru
1541 fi
1542 }
1543}
1544
1529if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then 1545if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
1530 ${DEBUG:-false} && set -x 1546 ${DEBUG:-false} && set -x
1531 run bollux "$@" 1547 run bollux "$@"