about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbollux759
1 files changed, 447 insertions, 312 deletions
diff --git a/bollux b/bollux index 2de37ab..ebdb22f 100755 --- a/bollux +++ b/bollux
@@ -1,8 +1,9 @@
1#!/usr/bin/env bash 1#!/usr/bin/env bash
2# bollux: a bash gemini client 2################################################################################
3# BOLLUX: a bash gemini client
3# Author: Case Duckworth 4# Author: Case Duckworth
4# License: MIT 5# License: MIT
5# Version: 0.4.0 6# Version: 0.4.1
6# 7#
7# Commentary: 8# Commentary:
8# 9#
@@ -46,6 +47,7 @@
46# [9]: OpenSSL `s_client' online manual 47# [9]: OpenSSL `s_client' online manual
47# https://www.openssl.org/docs/manmaster/man1/openssl-s_client.html 48# https://www.openssl.org/docs/manmaster/man1/openssl-s_client.html
48# 49#
50################################################################################
49# Code: 51# Code:
50 52
51# Program information 53# Program information
@@ -62,139 +64,13 @@ usage:
62flags: 64flags:
63 -h show this help and exit 65 -h show this help and exit
64 -q be quiet: log no messages 66 -q be quiet: log no messages
65 -v verbose: log more messages 67 -v be verbose: log more messages
66parameters: 68parameters:
67 URL the URL to start in 69 URL the URL to start in
68 If not provided, the user will be prompted. 70 If not provided, the user will be prompted.
69END 71END
70} 72}
71 73
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'. 74# Main entry point into `bollux'.
199# 75#
200# See the `if' block at the bottom of this script. 76# See the `if' block at the bottom of this script.
@@ -251,10 +127,15 @@ bollux_config() {
251 127
252 if [ -f "$BOLLUX_CONFIG" ]; then 128 if [ -f "$BOLLUX_CONFIG" ]; then
253 log debug "Loading config file '$BOLLUX_CONFIG'" 129 log debug "Loading config file '$BOLLUX_CONFIG'"
130 # Shellcheck gets mad when we try to source a file behind a
131 # variable -- it doesn't know where it is. This line ignores
132 # that warning, since the user can put $BOLLUX_CONFIG wherever.
254 # shellcheck disable=1090 133 # shellcheck disable=1090
255 . "$BOLLUX_CONFIG" 134 . "$BOLLUX_CONFIG"
256 else 135 else
257 log debug "Can't load config file '$BOLLUX_CONFIG'." 136 # It's an error if bollux can't find the config file, but I
137 # don't want to kill the program over it.
138 log error "Can't load config file '$BOLLUX_CONFIG'."
258 fi 139 fi
259 140
260 ## behavior 141 ## behavior
@@ -301,67 +182,185 @@ bollux_config() {
301 UC_BLANK=':?:' # internal use only, should be non-URL chars 182 UC_BLANK=':?:' # internal use only, should be non-URL chars
302} 183}
303 184
304# Load a URL. 185# Initialize bollux state
186bollux_init() {
187 # Trap `bollux_cleanup' on quit and exit
188 trap bollux_cleanup INT QUIT EXIT
189 # Trap `bollux_quit' on interrupt (C-c)
190 trap bollux_quit SIGINT
191
192 # Disable pathname expansion.
193 #
194 # It's very unlikely the user will want to navigate to a file when
195 # answering the GO prompt.
196 set -f
197
198 # Initialize state
199 #
200 # Other than $REDIRECTS, bollux's mutable state includes
201 # $BOLLUX_URL, but that's initialized elsewhere (possibly even by
202 # the user)
203 REDIRECTS=0
204
205 # History
206 #
207 # See also `history_append', `history_back', `history_forward'
208 declare -a HISTORY # history is kept in an array
209 HN=0 # position of history in the array
210 run mkdir -p "${BOLLUX_HISTFILE%/*}"
211
212 # Remove $BOLLUX_LESSKEY and re-generate keybindings (to catch rebinds)
213 run rm -f "$BOLLUX_LESSKEY"
214 mklesskey
215}
216
217# Cleanup on exit
218bollux_cleanup() {
219 # Stubbed in case of need in future
220 :
221}
222
223# Exit with success, printing a fun message.
305# 224#
306# I was feeling fancy when I named this function -- a more descriptive name 225# The default message is from the wonderful show "Cowboy Bebop."
307# would be 'bollux_goto' or something. 226bollux_quit() {
308blastoff() { # blastoff [-u] URL 227 printf '\e[1m%s\e[0m:\t\e[3m%s\e[0m\n' "$PRGN" "$BOLLUX_BYEMSG"
309 local u 228 exit
229}
310 230
311 # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with 231# UTILITY FUNCTIONS ############################################################
312 # a protocol string and no extraneous whitespace. Since bollux can't 232
313 # trust the user to input a proper URL at a prompt, nor capsule authors 233# Run a command, but log it first.
314 # to fully-form their URLs, so the -u flag is necessary for those 234#
315 # use-cases. Otherwise, bollux knows the URL is well-formed -- or 235# See `log' for the available levels.
316 # should be, due to the Gemini specification. 236run() { # run COMMAND...
237 # I have to add a `trap' here for SIGINT to work properly.
238 trap bollux_quit SIGINT
239 LOG_FUNC=2 log debug "> $*"
240 "$@"
241}
242
243# Log a message to stderr (&2).
244#
245# `log' in this script can take 3 different parameters: `d', `e', and `x', where
246# `x' is any other string (though I usually use `x'), followed by the message to
247# log. Most messages are either `d' (debug) level or `x' (diagnostic) level,
248# meaning I want to show them all the time or only when bollux is called with
249# `-v' (verbose). The levels are somewhat arbitrary, like I suspect all logging
250# levels are, but you can read the rest of bollux to see what I've chosen to
251# classify as what.
252log() { # log LEVEL MESSAGE...
253 # 'QUIET' means don't log anything.
254 [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return
255 local fmt # ANSI escape code
256
257 case "$1" in
258 ([dD]*) # Debug level -- only print if bollux -v.
259 [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return
260 fmt=34 # Blue
261 ;;
262 ([eE]*) # Error level -- always print.
263 fmt=31 # Red
264 ;;
265 (*) # Diagnostic level -- print unless QUIET.
266 fmt=1 # Bold
267 ;;
268 esac
269 shift
270
271 printf >&2 '\e[%sm%s:%-16s:\e[0m %s\n' \
272 "$fmt" "$PRGN" "${FUNCNAME[${LOG_FUNC:-1}]}" "$*"
273}
274
275# Exit with an error and a message describing it.
276die() { # die EXIT_CODE MESSAGE
277 local exit_code="$1"
278 shift
279 log error "$*"
280 exit "$exit_code"
281}
282
283# Trim leading and trailing whitespace from a string.
284#
285# [1]: #trim-leading-and-trailing-white-space-from-string
286trim_string() { # trim_string STRING
287 : "${1#"${1%%[![:space:]]*}"}"
288 : "${_%"${_##*[![:space:]]}"}"
289 printf '%s\n' "$_"
290}
291
292# Cycle a variable in a list given a delimiter.
293#
294# e.g. 'list_cycle one,two,three ,' => 'two,three,one'
295list_cycle() { # list_cycle LIST<string> DELIM
296 # I could've set up `list_cycle' to use an array instead of a delimited
297 # string, but the one variable this function is used for is
298 # T_PRE_DISPLAY, which is user-configurable. I wanted it to be as easy
299 # to configure for users who might not immediately know the bash array
300 # syntax, but can figure out 'variable=value' without much thought.
301 local list="${!1}" # Pass the list by name, not value
302 local delim="$2" # The delimiter of the string
303 local first="${list%%${delim}*}" # The first element
304 local rest="${list#*${delim}}" # The rest of the elements
305 # -v prints to the variable specified.
306 printf -v "$1" '%s%s%s' "${rest}" "${delim}" "${first}"
307}
308
309# Set the terminal title.
310set_title() { # set_title TITLE...
311 printf '\e]2;%s\007' "$*"
312}
313
314# Prompt the user for input.
315#
316# This is a thin wrapper around `read', a bash built-in. Because of the
317# way bollux messes around with stdin and stdout, I need to read directly from
318# the TTY with this function.
319prompt() { # prompt [-u] PROMPT [READ_ARGS...]
320 # `-e' gets the line "interactively", so it can see history and stuff
321 # `-r' reads a "raw" string, i.e., without backslash escaping
322 local read_cmd=(read -e -r)
317 if [[ "$1" == "-u" ]]; then 323 if [[ "$1" == "-u" ]]; then
318 u="$(run uwellform "$2")" 324 # `-i TEXT' uses TEXT as the initial text for `read'
319 else 325 read_cmd+=(-i "$BOLLUX_URL")
320 u="$1" 326 shift
321 fi 327 fi
328 local prompt="$1" # How to prompt the user
329 shift
330 read_cmd+=(-p "$prompt> ")
331 "${read_cmd[@]}" </dev/tty "$@"
332}
322 333
323 # After ensuring the URL is well-formed, `blastoff' needs to transform 334# Bash built-in replacement for `cat'
324 # it according to the transform rules of RFC 3986 (see §5.2.2), which 335#
325 # turns relative references into absolute references that bollux can use 336# 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 337# `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. 338# is really just here so I can say that bollux is written as purely in bash as
328 # 339# possible.
329 # Implementation detail: because Bash is really stupid when it comes to 340passthru() {
330 # arrays, the URL functions u* (see below) work with an array defined 341 while IFS= read -r; do
331 # with `local -a' and passed by name, not by value. Thus, the 342 printf '%s\n' "$REPLY"
332 # `urltransform url ...' instead of `urltransform "${url[@]}"' or 343 done
333 # similar. In addition, the `ucdef' and `ucset' functions take the name 344}
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 345
341 # To try and keep `bollux' as extensible as possible, I've written it 346# Bash built-in replacement for `sleep'
342 # only to expect two functions for every protocol it supports: 347#
343 # `x_request' and `x_response', where `x' is the name of the protocol 348# 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 349# function -- Dylan Araps did.
345 # for functions in the current scope, failing if it doesn't exist. 350#
346 # 351# [1]: #use-read-as-an-alternative-to-the-sleep-command
347 # In between `x_request' and `x_response', `blastoff' normalizes the 352sleep() { # sleep SECONDS
348 # line endings to UNIX-style (LF) for ease of display. 353 read -rt "$1" <> <(:) || :
349 { 354}
350 if declare -F "${url[1]}_request" >/dev/null 2>&1; then 355
351 run "${url[1]}_request" "$url" 356# Normalize files.
352 else 357normalize() {
353 die 99 "No request handler for '${url[1]}'" 358 shopt -s extglob # for the printf call below
354 fi 359 while IFS= read -r; do
355 } | run normalize | { 360 # Normalize line endings to Unix-style (LF)
356 if declare -F "${url[1]}_response" >/dev/null 2>&1; then 361 printf '%s\n' "${REPLY//$'\r'?($'\n')/}"
357 run "${url[1]}_response" "$url" 362 done
358 else 363 shopt -u extglob # reset 'extglob'
359 log d \
360 "No response handler for '${url[1]}';" \
361 " passing thru"
362 passthru
363 fi
364 }
365} 364}
366 365
367# URLS ######################################################################### 366# URLS #########################################################################
@@ -382,16 +381,16 @@ blastoff() { # blastoff [-u] URL
382# trim whitespace. 381# trim whitespace.
383# 382#
384# Useful for URLs that were probably input by humans. 383# Useful for URLs that were probably input by humans.
385uwellform() { 384uwellform() { # uwellform URL
386 local u="$1" 385 local url="$1"
387 386
388 if [[ "$u" != *://* ]]; then 387 if [[ "$url" != *://* ]]; then
389 u="$BOLLUX_PROTO://$u" 388 url="$BOLLUX_PROTO://$url"
390 fi 389 fi
391 390
392 u="$(trim_string "$u")" 391 url="$(trim_string "$url")"
393 392
394 printf '%s\n' "$u" 393 printf '%s\n' "$url"
395} 394}
396 395
397# Split a URL into its constituent parts, placing them all in the given array. 396# Split a URL into its constituent parts, placing them all in the given array.
@@ -406,58 +405,94 @@ uwellform() {
406# takes the matched URL, splits it using the regex, then assigns each part to an 405# takes the matched URL, splits it using the regex, then assigns each part to an
407# element of the url array NAME by using `printf -v', which prints to a 406# element of the url array NAME by using `printf -v', which prints to a
408# variable. 407# variable.
409usplit() { # usplit NAME:ARRAY URL:STRING 408usplit() { # usplit URL_ARRAY<name> URL
409 # Note: URL_ARRAY isn't assigned in `usplit', because it should
410 # already exist. Pass /only/ the name of URL_ARRAY to this
411 # function, not its contents.
410 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' 412 local re='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
411 [[ $2 =~ $re ]] || return $? 413 local u="$2"
414 [[ "$u" =~ $re ]] || {
415 exit_code=$?
416 log error "usplit: '$2' doesn't match '$re'"
417 return $?
418 }
412 419
413 # ShellCheck doesn't see that I'm using these variables in the `for' 420 # ShellCheck doesn't see that I'm using these variables in the `for'
414 # loop below, because I'm not technically using them /as/ variables, but 421 # loop below, because I'm not technically using them /as/ variables, but
415 # as names to the variables. The ${!c} formation in the `printf' call 422 # as names to the variables. The ${!c} formation in the `printf' call
416 # below performs a reverse lookup on the name to get the actual data. 423 # below performs a reverse lookup on the name to get the actual data.
417 # shellcheck disable=2034 424 # shellcheck disable=2034
418 local url="${BASH_REMATCH[0]}" \ 425 local entire_url="${BASH_REMATCH[0]}" \
419 scheme="${BASH_REMATCH[2]}" \ 426 scheme="${BASH_REMATCH[2]}" \
420 authority="${BASH_REMATCH[4]}" \ 427 authority="${BASH_REMATCH[4]}" \
421 path="${BASH_REMATCH[5]}" \ 428 path="${BASH_REMATCH[5]}" \
422 query="${BASH_REMATCH[7]}" \ 429 query="${BASH_REMATCH[7]}" \
423 fragment="${BASH_REMATCH[9]}" 430 fragment="${BASH_REMATCH[9]}"
424 431
432 # Iterate through the 5 components of a URL and assign them to elements
433 # of URL_ARRAY, as follows:
425 # 0=url 1=scheme 2=authority 3=path 4=query 5=fragment 434 # 0=url 1=scheme 2=authority 3=path 4=query 5=fragment
426 local i=1 c 435 run printf -v "$1[0]" '%s' "$entire_url"
436 # This loop tests whether the component exists first -- if it
437 # doesn't, the special variable $UC_BLANK is used in the spot
438 # instead. Bash doesn't have a useful way of differentiating an
439 # /unset/ element of an array, versus an /empty/ element.
440 # The only exception is that 'path' component, which always exists
441 # in a URL (I think the simplest URL possible is '/', the empty
442 # path).
443 local i=1 # begin at 1 -- the full URL is [0].
427 for c in scheme authority path query fragment; do 444 for c in scheme authority path query fragment; do
428 if [[ "${!c}" || "$c" == path ]]; then 445 if [[ "${!c}" || "$c" == path ]]; then
429 printf -v "$1[$i]" '%s' "${!c}" 446 run printf -v "$1[$i]" '%s' "${!c}"
430 else 447 else
431 printf -v "$1[$i]" '%s' "$UC_BLANK" 448 run printf -v "$1[$i]" '%s' "$UC_BLANK"
432 fi 449 fi
433 ((i += 1)) 450 ((i += 1))
434 done 451 done
435 printf -v "$1[0]" '%s' "$url"
436}
437 452
438# Join a URL array (NAME) back into a string. 453}
439ujoin() { # ujoin NAME:ARRAY
440 local -n U="$1"
441 454
442 if ucdef U[1]; then 455# Join a URL array, split with `usplit', back into a string, assigning
443 printf -v U[0] "%s:" "${U[1]}" 456# it to the 0th element of the array.
457ujoin() { # ujoin URL_ARRAY<name>
458 # Here's the documentation for the '-n' flag:
459 #
460 # Give each name the nameref attribute, making it a name reference
461 # to another variable. That other variable is defined by the value of
462 # name. All references, assignments, and attribute modifications to
463 # name, except for those using or changing the -n attribute itself,
464 # are performed on the variable referenced by name's value. The
465 # nameref attribute cannot be applied to array variables.
466 #
467 # Pretty handy for passing-by-name! Except that last part -- "The
468 # nameref attribute cannot be applied to array variables." However,
469 # I've found a clever hack -- you can use 'printf -v' to print the
470 # value to the array element.
471 local -n URL_ARRAY="$1"
472
473 # For each possible URL component, check if it exists with `ucdef'.
474 # If it does, append it (with the correct component delimiter) to
475 # URL_ARRAY[0].
476 if ucdef URL_ARRAY[1]; then
477 printf -v URL_ARRAY[0] "%s:" "${URL_ARRAY[1]}"
444 fi 478 fi
445 479
446 if ucdef U[2]; then 480 if ucdef URL_ARRAY[2]; then
447 printf -v U[0] "${U[0]}//%s" "${U[2]}" 481 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}//%s" "${URL_ARRAY[2]}"
448 fi 482 fi
449 483
450 printf -v U[0] "${U[0]}%s" "${U[3]}" 484 # The path component is required.
485 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}%s" "${URL_ARRAY[3]}"
451 486
452 if ucdef U[4]; then 487 if ucdef URL_ARRAY[4]; then
453 printf -v U[0] "${U[0]}?%s" "${U[4]}" 488 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}?%s" "${URL_ARRAY[4]}"
454 fi 489 fi
455 490
456 if ucdef U[5]; then 491 if ucdef URL_ARRAY[5]; then
457 printf -v U[0] "${U[0]}#%s" "${U[5]}" 492 printf -v URL_ARRAY[0] "${URL_ARRAY[0]}#%s" "${URL_ARRAY[5]}"
458 fi 493 fi
459 494
460 log d "${U[0]}" 495 log d "${URL_ARRAY[0]}"
461} 496}
462 497
463# `ucdef' checks whether a URL component is blank or not -- if a component 498# `ucdef' checks whether a URL component is blank or not -- if a component
@@ -466,26 +501,39 @@ ujoin() { # ujoin NAME:ARRAY
466# not going to really be in a URL). I tried really hard to differentiate an 501# not going to really be in a URL). I tried really hard to differentiate an
467# unset array element from a simply empty one, but like, as far as I could tell, 502# unset array element from a simply empty one, but like, as far as I could tell,
468# you can't do that in Bash. 503# you can't do that in Bash.
469ucdef() { # ucdef NAME 504ucdef() { # ucdef COMPONENT<name>
470 [[ "${!1}" != "$UC_BLANK" ]] 505 local component="$1"
506 [[ "${!component}" != "$UC_BLANK" ]]
471} 507}
472 508
473# `ucblank' determines whether a URL component is blank (""), as opposed to 509# `ucblank' determines whether a URL component is blank (""), as opposed to
474# undefined. 510# undefined.
475ucblank() { # ucblank NAME 511ucblank() { # ucblank COMPONENT<name>
476 [[ -z "${!1}" ]] 512 local component="$1"
513 [[ -z "${!component}" ]]
477} 514}
478 515
479# `ucset' sets one component of a URL array and setting the 0th element to the 516# `ucset' sets one component of a URL array and setting the 0th element to the
480# new full URL. Use it instead of directly setting the array element with U[x], 517# new full URL. Use it instead of directly setting the array element with U[x],
481# because U[0] will fall out of sync with the rest of the contents. 518# because U[0] will fall out of sync with the rest of the contents.
482ucset() { # ucset NAME VALUE 519ucset() { # ucset URL_ARRAY_INDEX<name> NEW_VALUE
483 run eval "${1}='$2'" 520 local url_array_component="$1" # Of form 'URL_ARRAY[INDEX]'
484 run ujoin "${1/\[*\]/}" 521 local value="$2"
522
523 # Assign $value to $url_array_component.
524 #
525 # Wrapped in an 'eval' for the extra layer of indirection.
526 run eval "${url_array_component}='$value'"
527
528 # Rejoin the URL_ARRAY with the changed value.
529 #
530 # The substitution here strips the array index subscript (i.e.,
531 # URL[4] => URL), passing the name of the full array to `ujoin'.
532 run ujoin "${url_array_component/\[*\]/}"
485} 533}
486 534
487# [1]: encode a URL using percent-encoding. 535# [1]: Encode a URL using percent-encoding.
488uencode() { # uencode URL:STRING 536uencode() { # uencode URL
489 local LC_ALL=C 537 local LC_ALL=C
490 for ((i = 0; i < ${#1}; i++)); do 538 for ((i = 0; i < ${#1}; i++)); do
491 : "${1:i:1}" 539 : "${1:i:1}"
@@ -497,14 +545,14 @@ uencode() { # uencode URL:STRING
497 printf '\n' 545 printf '\n'
498} 546}
499 547
500# [1]: decode a percent-encoded URL. 548# [1]: Decode a percent-encoded URL.
501udecode() { # udecode URL:STRING 549udecode() { # udecode URL
502 : "${1//+/ }" 550 : "${1//+/ }"
503 printf '%b\n' "${_//%/\\x}" 551 printf '%b\n' "${_//%/\\x}"
504} 552}
505 553
506# Implement [2] § 5.2.4, "Remove Dot Segments" 554# Implement [2]: 5.2.4, "Remove Dot Segments".
507pundot() { # pundot PATH:STRING 555pundot() { # pundot PATH
508 local input="$1" 556 local input="$1"
509 local output 557 local output
510 while [[ "$input" ]]; do 558 while [[ "$input" ]]; do
@@ -527,28 +575,28 @@ pundot() { # pundot PATH:STRING
527 printf '%s\n' "${output//\/\//\//}" 575 printf '%s\n' "${output//\/\//\//}"
528} 576}
529 577
530# Implement [2] § 5.2.3, "Merge Paths" 578# Implement [2] Section 5.2.3, "Merge Paths".
531pmerge() { # pmerge BASE:ARRAY REFERENCE:ARRAY 579pmerge() { # pmerge BASE_PATH<name> REFERENCE_PATH<name>
532 local -n b="$1" 580 local -n base_path="$1"
533 local -n r="$2" 581 local -n reference_path="$2"
534 582
535 if ucblank r[3]; then 583 if ucblank reference_path[3]; then
536 printf '%s\n' "${b[3]//\/\//\//}" 584 printf '%s\n' "${base_path[3]//\/\//\//}"
537 return 585 return
538 fi 586 fi
539 587
540 if ucdef b[2] && ucblank b[3]; then 588 if ucdef base_path[2] && ucblank base_path[3]; then
541 printf '/%s\n' "${r[3]//\/\//\//}" 589 printf '/%s\n' "${reference_path[3]//\/\//\//}"
542 else 590 else
543 local bp="" 591 local bp=""
544 if [[ "${b[3]}" == */* ]]; then 592 if [[ "${base_path[3]}" == */* ]]; then
545 bp="${b[3]%/*}" 593 bp="${base_path[3]%/*}"
546 fi 594 fi
547 printf '%s/%s\n' "${bp%/}" "${r[3]#/}" 595 printf '%s/%s\n' "${bp%/}" "${reference_path[3]#/}"
548 fi 596 fi
549} 597}
550 598
551# `utransform' implements [2]6 § 5.2.2, "Transform Resources." 599# `utransform' implements [2]6 Section 5.2.2, "Transform Resources."
552# 600#
553# That section conveniently lays out a pseudocode algorithm describing how URL 601# That section conveniently lays out a pseudocode algorithm describing how URL
554# resources should be transformed from one to another. This function just 602# resources should be transformed from one to another. This function just
@@ -624,19 +672,21 @@ utransform() { # utransform TARGET:ARRAY BASE:STRING REFERENCE:STRING
624# 672#
625################################################################################ 673################################################################################
626 674
627# Request a resource from a gemini server - see [3] §§ 2, 4. 675# Request a resource from a gemini server - see [3] Sections 2, 4.
628gemini_request() { # gemini_request URL 676gemini_request() { # gemini_request URL
629 local -a url 677 local -a url
630 usplit url "$1" 678 run usplit url "$1"
679 log debug "${url[@]}"
631 680
632 # Remove user info from the URL. 681 # Remove user info from the URL.
633 # 682 #
634 # URLs can technically be of the form <proto>://<user>:<pass>@<domain> 683 # URLs can technically be of the form <proto>://<user>:<pass>@<domain>
635 # (see [2], § 3.2, "Authority"). I don't know of any Gemini servers 684 # (see [2] Section 3.2, "Authority"). I don't know of any Gemini servers
636 # that use the <user> or <pass> parts, so `gemini_request' just strips 685 # that use the <user> or <pass> parts, so `gemini_request' just strips
637 # them from the requested URL. This will need to be changed if servers 686 # them from the requested URL. This will need to be changed if servers
638 # decide to use this method of authentication. 687 # decide to use this method of authentication.
639 ucset url[2] "${url[2]#*@}" 688 log debug "Removing user info from the URL"
689 run ucset url[2] "${url[2]#*@}"
640 690
641 # Determine the port to request. 691 # Determine the port to request.
642 # 692 #
@@ -645,6 +695,7 @@ gemini_request() { # gemini_request URL
645 # port can be specified after the domain, separated with a colon. The 695 # port can be specified after the domain, separated with a colon. The
646 # user can also request a different default port, for whatever reason, 696 # user can also request a different default port, for whatever reason,
647 # by setting the variable $BOLLUX_GEMINI_PORT. 697 # by setting the variable $BOLLUX_GEMINI_PORT.
698 log debug "Determining the port to request"
648 local port 699 local port
649 if [[ "${url[2]}" == *:* ]]; then 700 if [[ "${url[2]}" == *:* ]]; then
650 port="${url[2]#*:}" 701 port="${url[2]#*:}"
@@ -680,7 +731,7 @@ gemini_request() { # gemini_request URL
680 run "${ssl_cmd[@]}" <<<"$url" 731 run "${ssl_cmd[@]}" <<<"$url"
681} 732}
682 733
683# Handle the gemini response - see [3] § 3. 734# Handle the gemini response - see [3] Section 3.
684gemini_response() { # gemini_response URL 735gemini_response() { # gemini_response URL
685 local code meta # received on the first line of the response 736 local code meta # received on the first line of the response
686 local title # determined by a clunky heuristic, see read loop: (2*) 737 local title # determined by a clunky heuristic, see read loop: (2*)
@@ -700,12 +751,12 @@ gemini_response() { # gemini_response URL
700 # `download', below), but I'm not sure how to remedy that issue either. 751 # `download', below), but I'm not sure how to remedy that issue either.
701 # It requires more research. 752 # It requires more research.
702 while read -t "$BOLLUX_TIMEOUT" -r code meta || 753 while read -t "$BOLLUX_TIMEOUT" -r code meta ||
703 { (($? > 128)) && die 99 "Timeout."; }; do 754 { (($? > 128)) && die 99 "Timeout."; }; do
704 break 755 break
705 done 756 done
706 log d "[$code] $meta" 757 log d "[$code] $meta"
707 758
708 # Branch depending on the status code. See [3], Appendix 1. 759 # Branch depending on the status code. See [3] Appendix 1.
709 # 760 #
710 # Notes: 761 # Notes:
711 # - All codes other than 3* (Redirects) reset the REDIRECTS counter. 762 # - All codes other than 3* (Redirects) reset the REDIRECTS counter.
@@ -735,7 +786,7 @@ gemini_response() { # gemini_response URL
735 # 786 #
736 # This while loop reads through the file looking for a line 787 # This while loop reads through the file looking for a line
737 # starting with `#', which is a level-one heading in text/gemini 788 # starting with `#', which is a level-one heading in text/gemini
738 # (see [3], § 5). It assumes that the first such heading is the 789 # (see [3] Section 5). It assumes that the first such heading is the
739 # title of the page, and uses that title for the terminal title 790 # title of the page, and uses that title for the terminal title
740 # and for the history. 791 # and for the history.
741 local pretitle 792 local pretitle
@@ -771,7 +822,7 @@ gemini_response() { # gemini_response URL
771 # distinction. I'm not sure what the difference would be in 822 # distinction. I'm not sure what the difference would be in
772 # practice, anyway. 823 # practice, anyway.
773 # 824 #
774 # Per [4], bollux limits the number of redirects a page is 825 # Per [4] bollux limits the number of redirects a page is
775 # allowed to make (by default, five). Change `$BOLLUX_MAXREDIR' 826 # allowed to make (by default, five). Change `$BOLLUX_MAXREDIR'
776 # to customize that limit. 827 # to customize that limit.
777 ((REDIRECTS += 1)) 828 ((REDIRECTS += 1))
@@ -788,7 +839,7 @@ gemini_response() { # gemini_response URL
788 run blastoff "$meta" # TODO: confirm redirect 839 run blastoff "$meta" # TODO: confirm redirect
789 ;; 840 ;;
790 (4*) # TEMPORARY ERROR 841 (4*) # TEMPORARY ERROR
791 # Since the 4* codes ([3], Appendix 1) are all server issues, 842 # Since the 4* codes ([3] Appendix 1) are all server issues,
792 # bollux can treat them all basically the same. This is an area 843 # bollux can treat them all basically the same. This is an area
793 # that could use some expansion. 844 # that could use some expansion.
794 local desc="Temporary error" 845 local desc="Temporary error"
@@ -862,7 +913,7 @@ gemini_response() { # gemini_response URL
862gopher_request() { # gopher_request URL 913gopher_request() { # gopher_request URL
863 local url="$1" 914 local url="$1"
864 915
865 # [7] § 2.1 916 # [7] Section 2.1
866 [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] 917 [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]]
867 local server="${BASH_REMATCH[1]}" \ 918 local server="${BASH_REMATCH[1]}" \
868 port="${BASH_REMATCH[3]:-$BOLLUX_GOPHER_PORT}" \ 919 port="${BASH_REMATCH[3]:-$BOLLUX_GOPHER_PORT}" \
@@ -881,7 +932,7 @@ gopher_request() { # gopher_request URL
881# Handle a server response. 932# Handle a server response.
882gopher_response() { # gopher_response URL 933gopher_response() { # gopher_response URL
883 local url="$1" pre=false 934 local url="$1" pre=false
884 # [7] § 2.1 935 # [7] Section 2.1
885 # 936 #
886 # Note that this duplicates the code in `gopher_request'. There might 937 # Note that this duplicates the code in `gopher_request'. There might
887 # be a good way to thread this data through so that it's not computed 938 # be a good way to thread this data through so that it's not computed
@@ -896,7 +947,7 @@ gopher_response() { # gopher_response URL
896 # basically, each line in a gophermap starts with a character, its type, 947 # basically, each line in a gophermap starts with a character, its type,
897 # and then is followed by a series of tab-separated fields describing 948 # and then is followed by a series of tab-separated fields describing
898 # where that type is and how to display it. The full list of original 949 # where that type is and how to display it. The full list of original
899 # line types can be found in [6] § 3.8, though the types have also been 950 # line types can be found in [6] Section 3.8, though the types have also been
900 # extended over the years. Since bollux can only display types that are 951 # extended over the years. Since bollux can only display types that are
901 # text-ish, it only concerns itself with those in this case statement. 952 # text-ish, it only concerns itself with those in this case statement.
902 # All the others are simply downloaded. 953 # All the others are simply downloaded.
@@ -930,7 +981,7 @@ gopher_response() { # gopher_response URL
930 fi 981 fi
931 ;; 982 ;;
932 (*) # Anything else 983 (*) # Anything else
933 # The list at [6] § 3.8 includes the following (noted where it 984 # The list at [6] Section 3.8 includes the following (noted where it
934 # might be good to differently handle them in the future): 985 # might be good to differently handle them in the future):
935 # 986 #
936 # 2. Item is a CSO phone-book server ***** 987 # 2. Item is a CSO phone-book server *****
@@ -955,7 +1006,7 @@ gopher_response() { # gopher_response URL
955 1006
956# Convert a gophermap naively to a gemini page. 1007# Convert a gophermap naively to a gemini page.
957# 1008#
958# Based strongly on [8], but bash-ified. Due to the properties of link lines in 1009# Based strongly on [8] but bash-ified. Due to the properties of link lines in
959# gemini, many of the item types in `gemini_reponse' can be linked to the proper 1010# gemini, many of the item types in `gemini_reponse' can be linked to the proper
960# protocol handlers here -- so if a user is trying to reach a TCP link through 1011# protocol handlers here -- so if a user is trying to reach a TCP link through
961# gopher, bollux won't have to handle it, for example.* 1012# gopher, bollux won't have to handle it, for example.*
@@ -1013,7 +1064,7 @@ gopher_convert() {
1013 pre=false 1064 pre=false
1014 fi 1065 fi
1015 printf '=> telnet://%s:%s/%s%s %s\n' \ 1066 printf '=> telnet://%s:%s/%s%s %s\n' \
1016 "$server" "$port" "$type" "$path" "$label" 1067 "$server" "$port" "$type" "$path" "$label"
1017 ;; 1068 ;;
1018 (*) # other type 1069 (*) # other type
1019 if $pre; then 1070 if $pre; then
@@ -1021,7 +1072,7 @@ gopher_convert() {
1021 pre=false 1072 pre=false
1022 fi 1073 fi
1023 printf '=> gopher://%s:%s/%s%s %s\n' \ 1074 printf '=> gopher://%s:%s/%s%s %s\n' \
1024 "$server" "$port" "$type" "$path" "$label" 1075 "$server" "$port" "$type" "$path" "$label"
1025 ;; 1076 ;;
1026 esac 1077 esac
1027 done 1078 done
@@ -1043,7 +1094,8 @@ gopher_convert() {
1043# display the fetched content 1094# display the fetched content
1044display() { # display METADATA [TITLE] 1095display() { # display METADATA [TITLE]
1045 local -a less_cmd 1096 local -a less_cmd
1046 local i mime charset 1097 local mime charset
1098
1047 # split header line 1099 # split header line
1048 local -a hdr 1100 local -a hdr
1049 IFS=';' read -ra hdr <<<"$1" 1101 IFS=';' read -ra hdr <<<"$1"
@@ -1156,16 +1208,6 @@ END
1156 fi 1208 fi
1157} 1209}
1158 1210
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 1211# typeset a text/gemini document
1170typeset_gemini() { 1212typeset_gemini() {
1171 local pre=false 1213 local pre=false
@@ -1203,7 +1245,7 @@ typeset_gemini() {
1203 ;; 1245 ;;
1204 (alt | both) 1246 (alt | both)
1205 $pre && PRE_LINE_FORCE=true \ 1247 $pre && PRE_LINE_FORCE=true \
1206 gemini_pre "${REPLY#\`\`\`}" 1248 gemini_pre "${REPLY#\`\`\`}"
1207 ;; 1249 ;;
1208 esac 1250 esac
1209 continue 1251 continue
@@ -1240,13 +1282,13 @@ gemini_link() {
1240 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1282 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1241 printf "\e[${C_LINK_NUMBER}m[%d]${C_RESET} " "$ln" 1283 printf "\e[${C_LINK_NUMBER}m[%d]${C_RESET} " "$ln"
1242 fold_line -n -B "\e[${C_LINK_TITLE}m" -A "${C_RESET}" \ 1284 fold_line -n -B "\e[${C_LINK_TITLE}m" -A "${C_RESET}" \
1243 -l "$((${#ln} + 3))" -m "${T_MARGIN}" \ 1285 -l "$((${#ln} + 3))" -m "${T_MARGIN}" \
1244 "$WIDTH" "$(trim_string "$t")" 1286 "$WIDTH" "$(trim_string "$t")"
1245 fold_line -B " \e[${C_LINK_URL}m" \ 1287 fold_line -B " \e[${C_LINK_URL}m" \
1246 -A "${C_RESET}" \ 1288 -A "${C_RESET}" \
1247 -l "$((${#ln} + 3 + ${#t}))" \ 1289 -l "$((${#ln} + 3 + ${#t}))" \
1248 -m "$((T_MARGIN + ${#ln} + 2))" \ 1290 -m "$((T_MARGIN + ${#ln} + 2))" \
1249 "$WIDTH" "$a" 1291 "$WIDTH" "$a"
1250 else 1292 else
1251 gemini_pre "$1" 1293 gemini_pre "$1"
1252 fi 1294 fi
@@ -1264,7 +1306,7 @@ gemini_header() {
1264 1306
1265 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1307 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1266 fold_line -B "\e[${hdrfmt}m" -A "${C_RESET}" -m "${T_MARGIN}" \ 1308 fold_line -B "\e[${hdrfmt}m" -A "${C_RESET}" -m "${T_MARGIN}" \
1267 "$WIDTH" "$t" 1309 "$WIDTH" "$t"
1268 else 1310 else
1269 gemini_pre "$1" 1311 gemini_pre "$1"
1270 fi 1312 fi
@@ -1279,7 +1321,7 @@ gemini_list() {
1279 1321
1280 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1322 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1281 fold_line -B "\e[${C_LIST}m" -A "${C_RESET}" -m "$T_MARGIN" \ 1323 fold_line -B "\e[${C_LIST}m" -A "${C_RESET}" -m "$T_MARGIN" \
1282 "$WIDTH" "$t" 1324 "$WIDTH" "$t"
1283 else 1325 else
1284 gemini_pre "$1" 1326 gemini_pre "$1"
1285 fi 1327 fi
@@ -1294,7 +1336,7 @@ gemini_quote() {
1294 1336
1295 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s" 1337 printf "\e[${C_SIGIL}m%${S_MARGIN}s ${C_RESET}" "$s"
1296 fold_line -B "\e[${C_QUOTE}m" -A "${C_RESET}" -m "$T_MARGIN" \ 1338 fold_line -B "\e[${C_QUOTE}m" -A "${C_RESET}" -m "$T_MARGIN" \
1297 "$WIDTH" "$t" 1339 "$WIDTH" "$t"
1298 else 1340 else
1299 gemini_pre "$1" 1341 gemini_pre "$1"
1300 fi 1342 fi
@@ -1304,7 +1346,7 @@ gemini_text() {
1304 if ! ${2-false}; then 1346 if ! ${2-false}; then
1305 printf "%${S_MARGIN}s " ' ' 1347 printf "%${S_MARGIN}s " ' '
1306 fold_line -m "$T_MARGIN" \ 1348 fold_line -m "$T_MARGIN" \
1307 "$WIDTH" "$1" 1349 "$WIDTH" "$1"
1308 else 1350 else
1309 gemini_pre "$1" 1351 gemini_pre "$1"
1310 fi 1352 fi
@@ -1411,7 +1453,7 @@ handle_keypress() { # handle_keypress CODE
1411 run blastoff -u "$REPLY" 1453 run blastoff -u "$REPLY"
1412 ;; 1454 ;;
1413 (54) # ` - change alt-text visibility and refresh 1455 (54) # ` - change alt-text visibility and refresh
1414 run cycle_list T_PRE_DISPLAY , 1456 run list_cycle T_PRE_DISPLAY ,
1415 run blastoff "$BOLLUX_URL" 1457 run blastoff "$BOLLUX_URL"
1416 ;; 1458 ;;
1417 (55) # 55-57 -- still available for binding 1459 (55) # 55-57 -- still available for binding
@@ -1457,7 +1499,19 @@ extract_links() {
1457 done 1499 done
1458} 1500}
1459 1501
1460# download $BOLLUX_URL 1502# Download a file.
1503#
1504# Any non-otherwise-handled MIME type will be downloaded using this function.
1505# It uses 'dd' to download the resource to a temporary file, then attempts to
1506# move it to $BOLLUX_DOWNDIR (by default, $PWD). If that's not possible (either
1507# because the target file already exists or the 'mv' invocation fails for some
1508# reason), `download' logs the error and alerts the user where the temporary
1509# file is saved.
1510#
1511# `download' works by reading the end of the pipe from `display', which means
1512# that sometimes, due to something with the way bash or while or ... something
1513# ... chunks the data, sometimes binary data gets corrupted. This is an area
1514# that requires more research.
1461download() { 1515download() {
1462 tn="$(mktemp)" 1516 tn="$(mktemp)"
1463 log x "Downloading: '$BOLLUX_URL' => '$tn'..." 1517 log x "Downloading: '$BOLLUX_URL' => '$tn'..."
@@ -1472,60 +1526,141 @@ download() {
1472 fi 1526 fi
1473} 1527}
1474 1528
1475# initialize bollux 1529# HISTORY #####################################################################
1476bollux_init() { 1530#
1477 # Trap cleanup 1531# While bollux saves history to a file ($BOLLUX_HISTFILE), it doesn't /do/
1478 trap bollux_cleanup INT QUIT EXIT 1532# anything with the history that's been saved. When I do implement the history
1479 # State 1533# functionality, it'll probably be on top of a file:// protocol, which will make
1480 REDIRECTS=0 1534# it very simple to also implement bookmarks and the previewing of pages. In
1481 set -f 1535# fact, I should be able to implement this change by the weekend (2021-03-07).
1482 # History 1536#
1483 declare -a HISTORY # history is kept in an array 1537###############################################################################
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 1538
1497# append a URL to history 1539# Append a URL to history.
1498history_append() { # history_append URL TITLE 1540history_append() { # history_append URL TITLE
1499 BOLLUX_URL="$1" 1541 local url="$1"
1500 # date/time, url, title (best guess) 1542 local title="$2"
1501 run printf '%(%FT%T)T\t%s\t%s\n' -1 "$1" "$2" >>"$BOLLUX_HISTFILE" 1543
1502 HISTORY[$HN]="$BOLLUX_URL" 1544 # Print the URL and its title (if given) to $BOLLUX_HISTFILE.
1545 local fmt=''
1546 fmt+='%(%FT%T)T\t' # %(_)T calls directly to 'strftime'.
1547 if (( $# == 2 )); then
1548 fmt+='%s\t' # $url
1549 fmt+='%s\n' # $title
1550 else
1551 fmt+='%s%s\n' # printf needs a field for every argument.
1552 fi
1553 run printf -- "$fmt" -1 "$url" "$title" >>"$BOLLUX_HISTFILE"
1554
1555 # Add the URL to the HISTORY array and increment the pointer.
1556 HISTORY[$HN]="$url"
1503 ((HN += 1)) 1557 ((HN += 1))
1558
1559 # Update $BOLLUX_URL.
1560 BOLLUX_URL="$url"
1504} 1561}
1505 1562
1506# move back in history (session) 1563# Move back in session history.
1507history_back() { 1564history_back() {
1508 log d "HN=$HN" 1565 log d "HN=$HN"
1566 # We need to subtract 2 from HN because it automatically increases by
1567 # one with each call to `history_append'. If we subtract 1, we'll just
1568 # be at the end of the array again, reloading the page.
1509 ((HN -= 2)) 1569 ((HN -= 2))
1570
1510 if ((HN < 0)); then 1571 if ((HN < 0)); then
1511 HN=0 1572 HN=0
1512 log e "Beginning of history." 1573 log e "Beginning of history."
1513 return 1 1574 return 1
1514 fi 1575 fi
1576
1515 run blastoff "${HISTORY[$HN]}" 1577 run blastoff "${HISTORY[$HN]}"
1516} 1578}
1517 1579
1518# move forward in history (session) 1580# Move forward in session history.
1519history_forward() { 1581history_forward() {
1520 log d "HN=$HN" 1582 log d "HN=$HN"
1583
1521 if ((HN >= ${#HISTORY[@]})); then 1584 if ((HN >= ${#HISTORY[@]})); then
1522 HN="${#HISTORY[@]}" 1585 HN="${#HISTORY[@]}"
1523 log e "End of history." 1586 log e "End of history."
1524 return 1 1587 return 1
1525 fi 1588 fi
1589
1526 run blastoff "${HISTORY[$HN]}" 1590 run blastoff "${HISTORY[$HN]}"
1527} 1591}
1528 1592
1593# Load a URL.
1594#
1595# I was feeling fancy when I named this function -- a more descriptive name
1596# would be 'bollux_goto' or something.
1597blastoff() { # blastoff [-u] URL
1598 local u
1599
1600 # `blastoff' assumes a "well-formed" URL by default -- i.e., a URL with
1601 # a protocol string and no extraneous whitespace. Since bollux can't
1602 # trust the user to input a proper URL at a prompt, nor capsule authors
1603 # to fully-form their URLs, so the -u flag is necessary for those
1604 # use-cases. Otherwise, bollux knows the URL is well-formed -- or
1605 # should be, due to the Gemini specification.
1606 if [[ "$1" == "-u" ]]; then
1607 u="$(run uwellform "$2")"
1608 else
1609 u="$1"
1610 fi
1611
1612 # After ensuring the URL is well-formed, `blastoff' needs to transform
1613 # it according to the transform rules of RFC 3986 (see Section 5.2.2), which
1614 # turns relative references into absolute references that bollux can use
1615 # in its request to the server. That's followed by a check that the
1616 # protocol is set, defaulting to Gemini if it isn't.
1617 #
1618 # Implementation detail: because Bash is really stupid when it comes to
1619 # arrays, the URL functions u* (see below) work with an array defined
1620 # with `local -a' and passed by name, not by value. Thus, the
1621 # `urltransform url ...' instead of `urltransform "${url[@]}"' or
1622 # similar. In addition, the `ucdef' and `ucset' functions take the name
1623 # of the array element as parameters, not the element itself.
1624 local -a url
1625 run utransform url "$BOLLUX_URL" "$u"
1626 if ! ucdef url[1]; then
1627 run ucset url[1] "$BOLLUX_PROTO"
1628 fi
1629
1630 # To try and keep `bollux' as extensible as possible, I've written it
1631 # only to expect two functions for every protocol it supports:
1632 # `x_request' and `x_response', where `x' is the name of the protocol
1633 # (the first element of the built `url' array). `declare -F' looks only
1634 # for functions in the current scope, failing if it doesn't exist.
1635 #
1636 # In between `x_request' and `x_response', `blastoff' normalizes the
1637 # line endings to UNIX-style (LF) for ease of display.
1638 {
1639 if declare -F "${url[1]}_request" >/dev/null 2>&1; then
1640 run "${url[1]}_request" "$url"
1641 else
1642 die 99 "No request handler for '${url[1]}'"
1643 fi
1644 } | run normalize | {
1645 if declare -F "${url[1]}_response" >/dev/null 2>&1; then
1646 run "${url[1]}_response" "$url"
1647 else
1648 log d \
1649 "No response handler for '${url[1]}';" \
1650 " passing thru"
1651 passthru
1652 fi
1653 }
1654}
1655
1656# $BASH_SOURCE is an array that stores the "stack" of source calls in bash. If
1657# the first element of that array is "bollux", that means the user called this
1658# script, instead of sourcing it. In that case, and ONLY in that case, should
1659# bollux actually enter the main loop of the program. Otherwise, allow the
1660# sourcing environment to simply source this script.
1661#
1662# This is basically the equivalent of python's 'if __name__ == "__main__":'
1663# block.
1529if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then 1664if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
1530 ${DEBUG:-false} && set -x 1665 ${DEBUG:-false} && set -x
1531 run bollux "$@" 1666 run bollux "$@"