diff options
author | Case Duckworth | 2020-05-25 21:37:55 -0500 |
---|---|---|
committer | Case Duckworth | 2020-05-25 21:37:55 -0500 |
commit | 709334eb969c0161af6d78e0b708a09e164521b4 (patch) | |
tree | b6707aa94f1931fc06b1b0f845701fa3f6c45c80 | |
parent | Update README (diff) | |
download | bollux-709334eb969c0161af6d78e0b708a09e164521b4.tar.gz bollux-709334eb969c0161af6d78e0b708a09e164521b4.zip |
Major rewrite, v0.1
-rwxr-xr-x | bollux | 565 |
1 files changed, 278 insertions, 287 deletions
diff --git a/bollux b/bollux index 7f7459c..abd0ab9 100755 --- a/bollux +++ b/bollux | |||
@@ -1,117 +1,155 @@ | |||
1 | #!/usr/bin/env bash | 1 | #!/usr/bin/env bash |
2 | # bollux: a bash gemini client or whatever | 2 | |
3 | # Author: Case Duckworth <acdw@acdw.net> | 3 | # Program information |
4 | # License: MIT | 4 | PRGN="${0##*/}" |
5 | # Version: -0.7 | 5 | VRSN=0.1 |
6 | 6 | # State | |
7 | # set -euo pipefail # strict mode | 7 | REDIRECTS=0 |
8 | 8 | ||
9 | ### constants ### | 9 | run() { |
10 | PRGN="${0##*/}" # program name | 10 | log debug "$@" |
11 | DLDR="${BOLLUX_DOWNDIR:=.}" # where to download | 11 | "$@" |
12 | LOGL="${BOLLUX_LOGLEVEL:=3}" # log level | 12 | } |
13 | MAXR="${BOLLUX_MAXREDIR:=5}" # max redirects | 13 | |
14 | PORT="${BOLLUX_PORT:=1965}" # port number | 14 | die() { |
15 | PROT="${BOLLUX_PROTO:=gemini}" # protocol | 15 | ec="$1" |
16 | RDRS=0 # redirects | 16 | shift |
17 | VRSN=-0.7 # version number | 17 | log error "$*" |
18 | 18 | exit "$ec" | |
19 | # shellcheck disable=2120 | ||
20 | bollux_usage() { | ||
21 | cat <<END_USAGE >&2 | ||
22 | $PRGN ($VRSN): a bash gemini client | ||
23 | usage: | ||
24 | $PRGN [-h] | ||
25 | $PRGN [-L LVL] [URL] | ||
26 | options: | ||
27 | -h show this help | ||
28 | -L LVL set the loglevel to LVL. | ||
29 | Default: $BOLLUX_LOGLEVEL | ||
30 | The loglevel is between 0 and 5, with | ||
31 | lower levels being more dire. | ||
32 | parameters: | ||
33 | URL the URL to navigate view or download | ||
34 | END_USAGE | ||
35 | exit "${1:-0}" | ||
36 | } | 19 | } |
37 | 20 | ||
38 | # LOGLEVELS: | 21 | trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; } |
39 | # 0 - application fatal error | 22 | |
40 | # 1 - application warning | 23 | log() { |
41 | # 2 - response error | 24 | [[ "$BOLLUX_LOGLEVEL" == QUIET ]] && return |
42 | # 3 - response logging | ||
43 | # 4 - application logging | ||
44 | # 5 - diagnostic | ||
45 | |||
46 | ### utility functions ### | ||
47 | # a better echo | ||
48 | put() { printf '%s\n' "$*"; } | ||
49 | |||
50 | # conditionally log events to stderr | ||
51 | # lower = more important | ||
52 | log() { # log [LEVEL] [<] MESSAGE | ||
53 | case "$1" in | 25 | case "$1" in |
54 | -) | 26 | d* | D*) # debug |
55 | lvl="-1" | 27 | [[ "$BOLLUX_LOGLEVEL" == DEBUG ]] || return |
56 | shift | 28 | fmt=34 |
57 | ;; | 29 | ;; |
58 | [0-5]) | 30 | e* | E*) # error |
59 | lvl="$1" | 31 | fmt=31 |
60 | shift | ||
61 | ;; | 32 | ;; |
62 | *) lvl=4 ;; | 33 | *) fmt=1 ;; |
63 | esac | 34 | esac |
35 | shift | ||
36 | printf >&2 '\e[%sm%s:\e[0m\t%s\n' "$fmt" "$PRGN" "$*" | ||
37 | } | ||
64 | 38 | ||
65 | output="$*" | 39 | # main entry point |
66 | if ((lvl < LOGL)); then | 40 | bollux() { |
67 | if (($# == 0)); then | 41 | run bollux_args "$@" |
68 | while IFS= read -r line; do | 42 | run bollux_config |
69 | output="$output${output:+$'\n'}$line" | 43 | |
70 | done | 44 | if [[ ! "${BOLLUX_URL:+isset}" ]]; then |
71 | fi | 45 | run prompt GO BOLLUX_URL |
72 | printf '\e[34m%s\e[0m:\t%s\n' "$PRGN" "$output" >&2 | ||
73 | fi | 46 | fi |
47 | |||
48 | run blastoff "$BOLLUX_URL" | ||
74 | } | 49 | } |
75 | 50 | ||
76 | # halt and catch fire | 51 | bollux_args() { |
77 | die() { # die [EXIT-CODE] MESSAGE | 52 | while getopts :vq OPT; do |
78 | case "$1" in | 53 | case "$OPT" in |
79 | [0-9]*) | 54 | v) BOLLUX_LOGLEVEL=DEBUG ;; |
80 | ec="$1" | 55 | q) BOLLUX_LOGLEVEL=QUIET ;; |
81 | shift | 56 | :) die 1 "Option -$OPTARG requires an argument" ;; |
82 | ;; | 57 | *) die 1 "Unknown option: -$OPTARG" ;; |
83 | *) ec=1 ;; | 58 | esac |
84 | esac | 59 | done |
60 | shift $((OPTIND - 1)) | ||
61 | if (($# == 1)); then | ||
62 | BOLLUX_URL="$1" | ||
63 | fi | ||
64 | } | ||
85 | 65 | ||
86 | log 0 "$*" | 66 | bollux_config() { |
87 | exit "$ec" | 67 | : "${BOLLUX_CONFIG:=${XDG_CONFIG_DIR:-$HOME/.config}/bollux/config}" |
68 | |||
69 | if [ -f "$BOLLUX_CONFIG" ]; then | ||
70 | # shellcheck disable=1090 | ||
71 | . "$BOLLUX_CONFIG" | ||
72 | else | ||
73 | log debug "Can't load config file '$BOLLUX_CONFIG'." | ||
74 | fi | ||
75 | |||
76 | : "${BOLLUX_DOWNDIR:=.}" # where to save downloads | ||
77 | : "${BOLLUX_LOGLEVEL:=3}" # log level | ||
78 | : "${BOLLUX_MAXREDIR:=5}" # max redirects | ||
79 | : "${BOLLUX_PORT:=1965}" # port number | ||
80 | : "${BOLLUX_PROTO:=gemini}" # default protocol | ||
81 | : "${BOLLUX_LESSKEY:=/tmp/bollux-lesskey}" # where to store binds | ||
82 | : "${BOLLUX_PAGESRC:=/tmp/bollux-src}" # where to save the page source | ||
83 | : "${BOLLUX_URL:=}" # start url | ||
88 | } | 84 | } |
89 | 85 | ||
90 | # ask the user for input | 86 | prompt() { |
91 | ask() { # ask PROMPT [READ_OPT...] | ||
92 | prompt="$1" | 87 | prompt="$1" |
93 | shift | 88 | shift |
94 | read </dev/tty -e -r -p "$prompt> " "$@" | 89 | read </dev/tty -e -r -p "$prompt> " "$@" |
95 | } | 90 | } |
96 | 91 | ||
97 | # fail if something isn't installed | 92 | blastoff() { # load a url |
98 | require() { hash "$1" 2>/dev/null || die 127 "Requirement '$1' not found."; } | 93 | local well_formed=true |
94 | if [[ "$1" == "-u" ]]; then | ||
95 | well_formed=false | ||
96 | shift | ||
97 | fi | ||
98 | URL="$1" | ||
99 | |||
100 | if $well_formed && [[ "$1" != "$BOLLUX_URL" ]]; then | ||
101 | URL="$(run munge_url "$1" "$BOLLUX_URL")" | ||
102 | fi | ||
103 | [[ "$URL" != *://* ]] && URL="$BOLLUX_PROTO://$URL" | ||
104 | URL="$(trim <<<"$URL")" | ||
99 | 105 | ||
100 | # trim a string | 106 | server="${URL#*://}" |
101 | trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; } | 107 | server="${server%%/*}" |
108 | |||
109 | run request_url "$server" "$BOLLUX_PORT" "$URL" | | ||
110 | run handle_response "$URL" | ||
111 | } | ||
112 | |||
113 | munge_url() { | ||
114 | local -A new old u | ||
115 | eval "$(split_url new <<<"$1")" | ||
116 | for k in "${!new[@]}"; do log d "new[$k]=${new[$k]}"; done | ||
117 | eval "$(split_url old <<<"$2")" | ||
118 | for k in "${!old[@]}"; do log d "old[$k]=${old[$k]}"; done | ||
119 | |||
120 | u['scheme']="${new['scheme']:-${old['scheme']:-}}" | ||
121 | u['authority']="${new['authority']:-${old['authority']:-}}" | ||
122 | # XXX this whole path thing is wack | ||
123 | if [[ "${new['path']+isset}" ]]; then | ||
124 | log d 'new path set' | ||
125 | if [[ "${new['path']}" == /* ]]; then | ||
126 | log d 'new path == /*' | ||
127 | u['path']="${new['path']}" | ||
128 | elif [[ "${new['authority']}" == "${old['authority']}" || ! "${new['authority']+isset}" ]]; then | ||
129 | p="${old['path']:-}/${new['path']}" | ||
130 | log d "$p ( $(normalize_path <<<"$p") )" | ||
131 | u['path']="$(normalize_path <<<"$p")" | ||
132 | else | ||
133 | log d 'u path = new path' | ||
134 | u['path']="${new['path']}" | ||
135 | fi | ||
136 | elif [[ "${new['query']+isset}" || "${new['fragment']+isset}" ]]; then | ||
137 | log d 'u path = old path' | ||
138 | u['path']="${old['path']}" | ||
139 | else | ||
140 | u['path']="/" | ||
141 | fi | ||
142 | u['query']="${new['query']:-}" | ||
143 | u['fragment']="${new['fragment']:-}" | ||
144 | for k in "${!u[@]}"; do log d "u[$k]=${u[$k]}"; done | ||
102 | 145 | ||
103 | # stubs for when things aren't implemented (fully) | 146 | run printf '%s%s%s%s%s\n' \ |
104 | NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; } | 147 | "${u['scheme']}" "${u['authority']}" "${u['path']}" \ |
105 | NOT_FULLY_IMPLEMENTED() { log 1 "NOT FULLY IMPLEMENTED!!!"; } | 148 | "${u['query']}" "${u['fragment']}" |
149 | } | ||
106 | 150 | ||
107 | ### gemini ### | 151 | normalize_path() { |
108 | # url functions | ||
109 | # normalize a path from /../ /./ / | ||
110 | normalize_path() { # normalize_path <<< PATH | ||
111 | gawk '{ | 152 | gawk '{ |
112 | if ($0 == "" || $0 ~ /^\/\/[^\/]/) { | ||
113 | return -1 | ||
114 | } | ||
115 | split($0, path, /\//) | 153 | split($0, path, /\//) |
116 | for (c in path) { | 154 | for (c in path) { |
117 | if (path[c] == "" || path[c] == ".") { | 155 | if (path[c] == "" || path[c] == ".") { |
@@ -128,19 +166,18 @@ normalize_path() { # normalize_path <<< PATH | |||
128 | } | 166 | } |
129 | ret = ret slash path[c] | 167 | ret = ret slash path[c] |
130 | } | 168 | } |
131 | print ret | 169 | print (ret ~ /^\// ? "" : "/") ret |
132 | }' | 170 | }' |
133 | } | 171 | } |
134 | 172 | ||
135 | # split a url into the URL array | ||
136 | split_url() { | 173 | split_url() { |
137 | gawk '{ | 174 | gawk -vvar="$1" '{ |
138 | if (match($0, /^[A-Za-z]+:/)) { | 175 | if (match($0, /^[A-Za-z]+:/)) { |
139 | arr["scheme"] = substr($0, RSTART, RLENGTH) | 176 | arr["scheme"] = substr($0, RSTART, RLENGTH) |
140 | $0 = substr($0, RLENGTH + 1) | 177 | $0 = substr($0, RLENGTH + 1) |
141 | } | 178 | } |
142 | if (match($0, /^\/\/[^\/?#]+?/) || (match($0, /^[^\/?#]+?/) && scheme)) { | 179 | if (match($0, /^\/\/[^\/?#]+?/) || (match($0, /^[^\/?#]+?/) && scheme)) { |
143 | arr["authority"] = substr($0, RSTART, RLENGTH) | 180 | arr["authority"] = substr($0, RSTART, RLENGTH) |
144 | $0 = substr($0, RLENGTH + 1) | 181 | $0 = substr($0, RLENGTH + 1) |
145 | } | 182 | } |
146 | if (match($0, /^\/?[^?#]+/)) { | 183 | if (match($0, /^\/?[^?#]+/)) { |
@@ -156,140 +193,72 @@ split_url() { | |||
156 | $0 = substr($0, RLENGTH + 1) | 193 | $0 = substr($0, RLENGTH + 1) |
157 | } | 194 | } |
158 | for (part in arr) { | 195 | for (part in arr) { |
159 | printf "URL[\"%s\"]=\"%s\"\n", part, arr[part] | 196 | sub(/[[:space:]]+$/, "", arr[part]) |
197 | printf var "[\"%s\"]=\"%s\"\n", part, arr[part] | ||
160 | } | 198 | } |
161 | }' | 199 | }' |
162 | } | 200 | } |
163 | 201 | ||
164 | # example.com => gemini://example.com/ | 202 | request_url() { |
165 | _address() { # _address URL | 203 | local server="$1" |
166 | addr="$1" | 204 | local port="$2" |
205 | local url="$3" | ||
167 | 206 | ||
168 | [[ "$addr" != *://* ]] && addr="$PROT://$addr" | 207 | ssl_cmd=(openssl s_client -crlf -quiet -connect "$server/$port") |
169 | trim <<<"$addr" | 208 | ssl_cmd+=(-servername "$server") # SNI |
209 | run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null | ||
170 | } | 210 | } |
171 | 211 | ||
172 | # return only the server part from an address, with the port added | 212 | handle_response() { |
173 | # gemini://example.com/path/to/file => example.com:1965 | 213 | local url="$1" code meta |
174 | _server() { | ||
175 | serv="$(_address "$1")" # normalize first | ||
176 | serv="${serv#*://}" | ||
177 | serv="${serv%%/*}" | ||
178 | if [[ "$serv" != *:* ]]; then | ||
179 | serv="$serv:$PORT" | ||
180 | fi | ||
181 | trim <<<"$serv" | ||
182 | } | ||
183 | |||
184 | # request a gemini page | ||
185 | # by default, extract the server from the url | ||
186 | request() { # request [-s SERVER] URL | ||
187 | case "$1" in | ||
188 | -s) | ||
189 | serv="$(_server "$2")" | ||
190 | addr="$(_address "$3")" | ||
191 | ;; | ||
192 | *) | ||
193 | serv="$(_server "$1")" | ||
194 | addr="$(_address "$1")" | ||
195 | ;; | ||
196 | esac | ||
197 | |||
198 | log 5 "serv: $serv" | ||
199 | log 5 "addr: $addr" | ||
200 | |||
201 | sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv") | ||
202 | # use SNI | ||
203 | sslcmd+=(-servername "${serv%:*}") | ||
204 | log "${sslcmd[@]}" | ||
205 | "${sslcmd[@]}" <<<"$addr" 2>/dev/null | ||
206 | } | ||
207 | 214 | ||
208 | # handle the response | 215 | while read -r -d $'\r' hdr; do |
209 | # cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt | 216 | code="$(gawk '{print $1}' <<<"$hdr")" |
210 | handle() { # handle URL < RESPONSE | 217 | meta="$( |
211 | URL="$1" | 218 | gawk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$hdr" |
212 | while read -d $'\r' -r head; do | 219 | )" |
213 | break # wait to read the first line | 220 | break |
214 | done | 221 | done |
215 | code="$(gawk '{print $1}' <<<"$head")" | ||
216 | meta="$(gawk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")" | ||
217 | 222 | ||
218 | log 5 "[$code] $meta" | 223 | log x "[$code] $meta" |
219 | 224 | ||
220 | case "$code" in | 225 | case "$code" in |
221 | 1*) # INPUT | 226 | 1*) |
222 | log 3 "Input" | 227 | REDIRECTS=0 |
223 | RDRS=0 # this is not a redirect | 228 | BOLLUX_URL="$URL" |
224 | ask "$meta" QUERY | 229 | run prompt "$meta" QUERY |
225 | bollux "$URL?$QUERY" | 230 | run blastoff "?$QUERY" |
226 | ;; | 231 | ;; |
227 | 2*) # SUCCESS | 232 | 2*) |
228 | log 3 "Success" | 233 | REDIRECTS=0 |
229 | RDRS=0 # this is not a redirect | 234 | BOLLUX_URL="$URL" |
230 | case "$code" in | 235 | run display "$meta" |
231 | 20) log 5 "- OK" ;; | ||
232 | 21) log 5 "- End of client certificate session" ;; | ||
233 | *) log 2 "- Unknown response code: '$code'." ;; | ||
234 | esac | ||
235 | display "$meta" | ||
236 | ;; | ||
237 | 3*) # REDIRECT | ||
238 | log 3 "Redirecting" | ||
239 | case "$code" in | ||
240 | 30) log 5 "- Temporary" ;; | ||
241 | 31) log 5 "- Permanent" ;; | ||
242 | *) log 2 "- Unknown response code: '$code'." ;; | ||
243 | esac | ||
244 | ((RDRS += 1)) | ||
245 | ((RDRS > MAXR)) && die "$code" "Too many redirects!" | ||
246 | bollux "$meta" | ||
247 | ;; | 236 | ;; |
248 | 4*) # TEMPORARY FAILURE | 237 | 3*) |
249 | log 2 "Temporary failure" | 238 | ((REDIRECTS += 1)) |
250 | RDRS=0 # this is not a redirect | 239 | if ((REDIRECTS > BOLLUX_MAXREDIR)); then |
251 | case "$code" in | 240 | die $((100 + code)) "Too many redirects!" |
252 | 41) log 5 "- Server unavailable" ;; | 241 | fi |
253 | 42) log 5 "- CGI error" ;; | 242 | BOLLUX_URL="$URL" |
254 | 43) log 5 "- Proxy error" ;; | 243 | run blastoff "$meta" |
255 | 44) log 5 "- Rate limited" ;; | ||
256 | *) log 2 "- Unknown response code: '$code'." ;; | ||
257 | esac | ||
258 | exit "$code" | ||
259 | ;; | 244 | ;; |
260 | 5*) # PERMANENT FAILURE | 245 | 4*) |
261 | log 2 "Permanent failure" | 246 | REDIRECTS=0 |
262 | RDRS=0 # this is not a redirect | 247 | die "$((100 + code))" "$code" |
263 | case "$code" in | ||
264 | 51) log 5 "- Not found" ;; | ||
265 | 52) log 5 "- No longer available" ;; | ||
266 | 53) log 5 "- Proxy request refused" ;; | ||
267 | 59) log 5 "- Bad request" ;; | ||
268 | *) log 2 "- Unknown response code: '$code'." ;; | ||
269 | esac | ||
270 | exit "$code" | ||
271 | ;; | 248 | ;; |
272 | 6*) # CLIENT CERT REQUIRED | 249 | 5*) |
273 | log 2 "Client certificate required" | 250 | REDIRECTS=0 |
274 | RDRS=0 # this is not a redirect | 251 | die "$((100 + code))" "$code" |
275 | case "$code" in | ||
276 | 61) log 5 "- Transient cert requested" ;; | ||
277 | 62) log 5 "- Authorized cert required" ;; | ||
278 | 63) log 5 "- Cert not accepted" ;; | ||
279 | 64) log 5 "- Future cert rejected" ;; | ||
280 | 65) log 5 "- Expired cert rejected" ;; | ||
281 | *) log 2 "- Unknown response code: '$code'." ;; | ||
282 | esac | ||
283 | exit "$code" | ||
284 | ;; | 252 | ;; |
285 | *) # ??? | 253 | 6*) |
286 | die "$code" "Unknown response code: '$code'." | 254 | REDIRECTS=0 |
255 | die "$((100 + code))" "$code" | ||
287 | ;; | 256 | ;; |
257 | *) die "$((100 + code)) Unknown response code: $code." ;; | ||
288 | esac | 258 | esac |
289 | } | 259 | } |
290 | 260 | ||
291 | # display the page | 261 | display() { |
292 | display() { # display META < DOCUMENT | ||
293 | case "$1" in | 262 | case "$1" in |
294 | *\;*) | 263 | *\;*) |
295 | mime="$(cut -d\; -f1 <<<"$1" | trim)" | 264 | mime="$(cut -d\; -f1 <<<"$1" | trim)" |
@@ -297,125 +266,147 @@ display() { # display META < DOCUMENT | |||
297 | ;; | 266 | ;; |
298 | *) mime="$(trim <<<"$1")" ;; | 267 | *) mime="$(trim <<<"$1")" ;; |
299 | esac | 268 | esac |
300 | [ -z "$mime" ] && mime="text/gemini" | 269 | |
301 | if [ -z "$charset" ]; then | 270 | [[ -z "$mime" ]] && mime="text/gemini" |
271 | if [[ -z "$charset" ]]; then | ||
302 | charset="utf-8" | 272 | charset="utf-8" |
303 | else | 273 | else |
304 | charset="${charset#*=}" | 274 | charset="${charset#charset=}" |
305 | fi | 275 | fi |
306 | 276 | ||
307 | log 5 "mime=$mime; charset=$charset" | 277 | log debug "mime=$mime; charset=$charset" |
308 | 278 | ||
309 | case "$mime" in | 279 | case "$mime" in |
310 | text/gemini) | 280 | text/*) |
311 | lc="/tmp/bollux-currentpage.gmi" # link copy | 281 | less_cmd=(less -R) |
312 | lfn | typeset_gemini | tee "$lc" | less -R || | 282 | { |
313 | cat "$lc" # TODO list out links on success | 283 | [[ -r "$BOLLUX_LESSKEY" ]] || mklesskey "$BOLLUX_LESSKEY" |
314 | # lesskey: | 284 | } && less_cmd+=(-k "$BOLLUX_LESSKEY") |
315 | # o #> Open a link (quit 1) | 285 | |
316 | # q #> Quit (quit 0) | 286 | submime="${mime#*/}" |
317 | # cf. also prompt & filename | 287 | if declare -F | grep -q "$submime"; then |
288 | log d "typeset_$submime" | ||
289 | { | ||
290 | normalize_crlf | | ||
291 | run "typeset_$submime" | | ||
292 | tee "$BOLLUX_PAGESRC" | | ||
293 | run "${less_cmd[@]}" | ||
294 | } || run handle_keypress "$?" | ||
295 | else | ||
296 | log "cat" | ||
297 | { | ||
298 | normalize_crlf | | ||
299 | tee "$BOLLUX_PAGESRC" | | ||
300 | run "${less_cmd[@]}" | ||
301 | } || run handle_keypress "$?" | ||
302 | fi | ||
318 | ;; | 303 | ;; |
319 | text/*) lfn ;; | 304 | *) run download "$BOLLUX_URL" ;; |
320 | *) download "$URL" ;; | ||
321 | esac | 305 | esac |
322 | } | 306 | } |
323 | 307 | ||
324 | # normalize line endings to \n (LF) | 308 | mklesskey() { |
325 | lfn() { | 309 | lesskey -o "$1" - <<-END |
310 | #command | ||
311 | o quit 0 # 48 open a link | ||
312 | g quit 1 # 49 goto a url | ||
313 | [ quit 2 # 50 back | ||
314 | ] quit 3 # 51 forward | ||
315 | r quit 4 # 52 re-request / download | ||
316 | END | ||
317 | } | ||
318 | |||
319 | normalize_crlf() { | ||
326 | gawk 'BEGIN{RS="\n\n"}{gsub(/\r\n?/,"\n");print;print ""}' | 320 | gawk 'BEGIN{RS="\n\n"}{gsub(/\r\n?/,"\n");print;print ""}' |
327 | } | 321 | } |
328 | 322 | ||
329 | # typeset text | 323 | typeset_gemini() { |
330 | typeset_gemini() { # typeset_gemini < INPUT | ||
331 | gawk ' | 324 | gawk ' |
332 | BEGIN { pre = 0 } | 325 | BEGIN { pre = 0 } |
333 | /^###/ { sub(/^#+[[:space:]]*/, ""); | 326 | /^###/ { sub(/^#+[[:space:]]*/, ""); |
334 | printf " \033[3m%s\033[0m\n", $0 | 327 | printf "### \033[3m%s\033[0m\n", $0 |
335 | next } | 328 | next } |
336 | /^##/ { sub(/^#+[[:space:]]*/, ""); | 329 | /^##/ { sub(/^#+[[:space:]]*/, ""); |
337 | printf " \033[1m%s\033[0m\n", $0 | 330 | printf "## \033[1m%s\033[0m\n", $0 |
338 | next } | 331 | next } |
339 | /^#/ { sub(/^#+[[:space:]]*/, ""); | 332 | /^#/ { sub(/^#+[[:space:]]*/, ""); |
340 | printf " \033[1;4m%s\033[0m\n", $0 | 333 | printf "# \033[1;4m%s\033[0m\n", $0 |
341 | next } | 334 | next } |
342 | /^=>/ { | 335 | /^=>/ { |
343 | sub(/=>[[:space:]]*/, "") | 336 | sub(/=>[[:space:]]*/, "") |
344 | url = $1; desc = "" | 337 | url = $1; desc = "" |
345 | for (w=2;w<=NF;w++) | 338 | for (w=2;w<=NF;w++) |
346 | desc = desc (desc?" ":"") $w | 339 | desc = desc (desc?" ":"") $w |
347 | printf " \033[1m[%s]\033[0m \033[4m%s\033[0m \033[36m%s\033[0m\n", | 340 | printf "=> \033[1m[%02d]\033[0m \033[4m%s\033[0m\t\033[36m%s\033[0m\n", |
348 | (++ln), desc, "(" url ")" | 341 | (++ln), desc, url |
349 | next } | 342 | next } |
350 | # /^\*/ { sub(/\*[[:space:]]*/, ""); } | ||
351 | /```/ { pre = !pre; next } | 343 | /```/ { pre = !pre; next } |
344 | pre { printf "``` %s\n", $0; next } | ||
345 | # /^\*/ { sub(/\*[[:space:]]*/, ""); } | ||
352 | { sub(/^/, " "); print } | 346 | { sub(/^/, " "); print } |
353 | ' | 347 | ' |
354 | } | 348 | } |
355 | 349 | ||
356 | download() { # download URL < FILE | 350 | handle_keypress() { |
357 | tn="$(mktemp)" | 351 | case "$1" in |
358 | dd status=progress >"$tn" | 352 | 48) # o - open a link -- show a menu of links on the page |
359 | fn="$DLDR/${URL##*/}" | 353 | run select_url "$BOLLUX_PAGESRC" |
360 | if [[ -f "$fn" ]]; then | 354 | ;; |
361 | log - "Saved '$tn'." | 355 | 49) # g - goto a url -- input a new url |
362 | else | 356 | prompt GO URL |
363 | if mv "$tn" "$fn"; then | 357 | run blastoff -u "$URL" |
364 | log - "Saved '$fn'." | 358 | ;; |
365 | else | 359 | 50) # [ - back in the history |
366 | log 0 "Error saving '$fn'." | 360 | run history_back |
367 | log - "Saved '$tn'." | 361 | ;; |
368 | fi | 362 | 51) # ] - forward in the history |
369 | fi | 363 | run history_forward |
364 | ;; | ||
365 | 52) # r - re-request the current resource | ||
366 | run blastoff "$BOLLUX_URL" | ||
367 | ;; | ||
368 | *) # 53-57 -- still available for binding | ||
369 | ;; | ||
370 | esac | ||
370 | } | 371 | } |
371 | 372 | ||
372 | ### main entry point ### | 373 | select_url() { |
373 | bollux() { | 374 | run mapfile -t < <(extract_links <"$1") |
374 | OPTIND=0 | 375 | select u in "${MAPFILE[@]}"; do |
375 | process_cmdline "$@" | 376 | run blastoff "$(gawk '{print $1}' <<<"$u")" && break |
376 | shift $((OPTIND - 1)) | 377 | done </dev/tty |
377 | |||
378 | if (($# == 1)); then | ||
379 | URL="$1" | ||
380 | else | ||
381 | ask GO URL | ||
382 | fi | ||
383 | |||
384 | log 5 "URL : $URL" | ||
385 | |||
386 | request "$URL" | handle "$URL" | ||
387 | } | 378 | } |
388 | 379 | ||
389 | process_cmdline() { | 380 | extract_links() { |
390 | while getopts :hL: OPT; do | 381 | gawk -F$'\t' '/^=>/ { |
391 | case "$OPT" in | 382 | gsub("\033\\[[^m]*m", "") |
392 | h) bollux_usage ;; | 383 | sub(/=>[[:space:]]*\[[0-9]+\][[:space:]]*/,"") |
393 | L) LOGL="$OPTARG" ;; | 384 | if ($2) |
394 | :) die 1 "Option -$OPTARG requires an argument" ;; | 385 | printf "%s (\033[34m%s\033[0m)\n", $2, $1 |
395 | *) die 1 "Unknown option: -$OPTARG" ;; | 386 | else |
396 | esac | 387 | printf "%s\n", $1 |
397 | done | 388 | }' |
398 | } | 389 | } |
399 | 390 | ||
400 | bollux_setup() { | 391 | download() { |
401 | trap bollux_cleanup INT QUIT EXIT | 392 | tn="$(mktemp)" |
393 | log x "Downloading: '$BOLLUX_URL' => '$tn'..." | ||
394 | dd status=progress >"$tn" | ||
395 | fn="$BOLLUX_DOWNDIR/${BOLLUX_URL##*/}" | ||
396 | if [[ -f "$fn" ]]; then | ||
397 | log x "Saved '$tn'." | ||
398 | elif mv "$tn" "$fn"; then | ||
399 | log x "Saved '$fn'." | ||
400 | else | ||
401 | log error "Error saving '$fn': downloaded to '$tn'." | ||
402 | fi | ||
402 | } | 403 | } |
403 | 404 | ||
404 | bollux_cleanup() { | 405 | history_back() { log error "Not implemented."; } |
405 | exit $? | 406 | history_forward() { log error "Not implemented."; } |
406 | } | ||
407 | 407 | ||
408 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | 408 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then |
409 | # requirements here -- so they're only checked once | 409 | run bollux "$@" |
410 | require gawk | 410 | else |
411 | require dd | 411 | BOLLUX_LOGLEVEL=DEBUG |
412 | require mv | ||
413 | require openssl | ||
414 | require sed | ||
415 | |||
416 | bollux_setup | ||
417 | |||
418 | bollux "$@" | ||
419 | |||
420 | bollux_cleanup | ||
421 | fi | 412 | fi |