diff options
-rwxr-xr-x | bollux | 759 |
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: | |||
62 | flags: | 64 | flags: |
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 |
66 | parameters: | 68 | parameters: |
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. |
69 | END | 71 | END |
70 | } | 72 | } |
71 | 73 | ||
72 | # UTILITY FUNCTIONS ############################################################ | ||
73 | |||
74 | # Run a command, but log it first. | ||
75 | # | ||
76 | # See `log' for the available levels. | ||
77 | run() { # 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. | ||
85 | die() { # 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." | ||
95 | bollux_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. | ||
100 | trap bollux_quit SIGINT | ||
101 | |||
102 | # Trim leading and trailing whitespace from a string. | ||
103 | # | ||
104 | # [1]: #trim-leading-and-trailing-white-space-from-string | ||
105 | trim_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' | ||
114 | cycle_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' | ||
124 | first() { # 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 | ||
132 | log() { # 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. | ||
152 | set_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. | ||
161 | prompt() { # 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. | ||
180 | passthru() { | ||
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 | ||
192 | sleep() { # 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 |
186 | bollux_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 | ||
218 | bollux_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. | 226 | bollux_quit() { |
308 | blastoff() { # 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. | 236 | run() { # 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. | ||
252 | log() { # 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. | ||
276 | die() { # 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 | ||
286 | trim_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' | ||
295 | list_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. | ||
310 | set_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. | ||
319 | prompt() { # 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 | 340 | passthru() { |
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 | 352 | sleep() { # 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 | 357 | normalize() { |
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. |
385 | uwellform() { | 384 | uwellform() { # 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. |
409 | usplit() { # usplit NAME:ARRAY URL:STRING | 408 | usplit() { # 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 | } |
439 | ujoin() { # 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. |
457 | ujoin() { # 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. |
469 | ucdef() { # ucdef NAME | 504 | ucdef() { # 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. |
475 | ucblank() { # ucblank NAME | 511 | ucblank() { # 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. |
482 | ucset() { # ucset NAME VALUE | 519 | ucset() { # 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. |
488 | uencode() { # uencode URL:STRING | 536 | uencode() { # 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. |
501 | udecode() { # udecode URL:STRING | 549 | udecode() { # 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". |
507 | pundot() { # pundot PATH:STRING | 555 | pundot() { # 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". |
531 | pmerge() { # pmerge BASE:ARRAY REFERENCE:ARRAY | 579 | pmerge() { # 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. |
628 | gemini_request() { # gemini_request URL | 676 | gemini_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. |
684 | gemini_response() { # gemini_response URL | 735 | gemini_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 | |||
862 | gopher_request() { # gopher_request URL | 913 | gopher_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. |
882 | gopher_response() { # gopher_response URL | 933 | gopher_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 |
1044 | display() { # display METADATA [TITLE] | 1095 | display() { # 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 | ||
1160 | normalize() { | ||
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 |
1170 | typeset_gemini() { | 1212 | typeset_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. | ||
1461 | download() { | 1515 | download() { |
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 ##################################################################### |
1476 | bollux_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 | ||
1492 | bollux_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. |
1498 | history_append() { # history_append URL TITLE | 1540 | history_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. |
1507 | history_back() { | 1564 | history_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. |
1519 | history_forward() { | 1581 | history_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. | ||
1597 | blastoff() { # 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. | ||
1529 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | 1664 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then |
1530 | ${DEBUG:-false} && set -x | 1665 | ${DEBUG:-false} && set -x |
1531 | run bollux "$@" | 1666 | run bollux "$@" |