astro (14355B)
1 #!/bin/sh 2 3 version="0.25.1" 4 5 usage() { 6 echo "astro v$version: Browse the gemini web on the terminal." 7 echo "" 8 echo "Usage: astro [URL]|[OPTION]" 9 echo "" 10 echo "Options:" 11 echo " -h, --help show this help" 12 echo " -v, --version show version info" 13 echo "" 14 echo "Configuration:" 15 echo "You can setup a config file at ~/.config/astro/astro.conf to configure *astro* the way you like." 16 echo "" 17 echo "Commands:" 18 echo "These are the default keybindings to use while running astro:" 19 echo "" 20 echo " q quit" 21 echo " g go to a link" 22 echo " r reload current page" 23 echo " b go back one page" 24 echo " u jump one path segment up" 25 echo " o open an address" 26 echo " s save current page" 27 echo " H go to homepage" 28 echo " m add bookmark" 29 echo " M go to a bookmark" 30 echo " K remove bookmark for current url" 31 echo "" 32 echo "Examples:" 33 echo " astro Start browsing the default webpage" 34 echo " astro url Start browsing url" 35 echo " astro --help Show help" 36 echo "" 37 echo "Report bugs to: bleemayer@gmail.com" 38 echo "Home page: <https://www.github.com/blmayer/astro/>" 39 echo "General help: <https://www.github.com/blmayer/astro/wiki>" 40 } 41 42 version() { 43 echo "astro $version" 44 echo "" 45 echo "Copyright (C) 2021-2023 Brian Mayer." 46 echo "License MIT: MIT License <https://opensource.org/licenses/MIT>" 47 echo "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND," 48 echo "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF" 49 echo "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT." 50 echo "" 51 echo "Written by Brian Lee Mayer." 52 } 53 54 debug() { 55 [ "$debug" ] && echo "DEBUG: $*" >&2 && sleep "$debug" 56 } 57 58 # Parse arguments 59 debug "parsing args" 60 while [ "$1" ] 61 do 62 case $1 in 63 "-v") 64 debug=1 65 ;; 66 "-h" | "--help") 67 usage 68 exit 0 69 ;; 70 "--version") 71 version 72 exit 0 73 ;; 74 "-f"|"--file") 75 shift 76 file="$1" 77 ;; 78 "") 79 break 80 ;; 81 *) 82 url="$1" 83 ;; 84 esac 85 shift 86 done 87 88 # Save terminal 89 tput smcup 90 91 # Configuration 92 confighome=${XDG_CONFIG_HOME:-$HOME/.config} 93 mkdir -p "$confighome/astro" 94 configfile="$confighome/astro/astro.conf" 95 bookmarkfile="$confighome/astro/bookmarks" 96 certdir="$confighome/astro/certs" 97 mkdir -p "$certdir" 98 99 cachedir="${XDG_CACHE_HOME:-$HOME/.cache}/astro" 100 101 # Default values 102 103 # user config 104 margin=8 105 homepage="geminiprotocol.net/" 106 107 # keybindings 108 openkey='o' 109 openlocalkey='O' 110 gokey='g' 111 refreshkey='r' 112 backkey='b' 113 quitkey='q' 114 markkey='b' 115 gomarkkey='M' 116 delmarkkey='K' 117 goupkey='u' 118 homekey='H' 119 120 # styles 121 sty_header1='\033[35;7;1m' 122 sty_header2='\033[35;4;1m' 123 sty_header3='\033[35;4m' 124 sty_quote='\033[2;3m ' 125 sty_linkb='\033[35m' 126 sty_linkt=' => \033[36;3m ' 127 sty_listb='\033[35;1m •' 128 sty_listt='\033[0m ' 129 130 if [ ! -s "$configfile" ] 131 then 132 # Default values 133 cat <<- EOF > "$configfile" 134 # where to store temp files 135 cachedir="$cachedir" 136 137 # user config 138 margin=8 139 homepage="gemini.circumlunar.space/" 140 141 # keybindings 142 openkey='o' 143 openlocalkey='O' 144 gokey='g' 145 refreshkey='r' 146 backkey='b' 147 quitkey='q' 148 markkey='b' 149 gomarkkey='M' 150 delmarkkey='K' 151 goupkey='u' 152 homekey='H' 153 154 # styles 155 sty_header1='\033[35;7;1m' 156 sty_header2='\033[35;4;1m' 157 sty_header3='\033[35;4m' 158 sty_quote='\033[2;3m ' 159 sty_linkb='\033[35m' 160 sty_linkt=' => \033[36;3m ' 161 sty_listb='\033[35;1m •' 162 sty_listt='\033[0m ' 163 EOF 164 fi 165 166 # shellcheck source=/dev/null 167 . "$configfile" 168 169 mkdir -p "$cachedir" 170 pagefile="$(mktemp -p "$cachedir" -t curpage.XXXXXX)" 171 histfile="$(mktemp -p "$cachedir" -t history.XXXXXX)" 172 linksfile="$(mktemp -p "$cachedir" -t links.XXXXXX)" 173 tracefile="$(mktemp -p "$cachedir" -t trace.XXXXXX)" 174 debug "read configs" 175 176 # Restore terminal 177 trap 'rm -f $histfile $linksfile $pagefile $tracefile > /dev/null 2>&1 && tput rmcup && printf "\033[?25h" && stty echo && exit' EXIT INT HUP 178 179 stop() { 180 [ "$trace" ] || return 181 if [ -z "$stopwatch" ] 182 then 183 stopwatch=$(date +%s.%N) 184 else 185 dur=$(echo "$(date +%s.%N) - $stopwatch" | bc) 186 printf "%s took %s seconds\n" "$1" "$dur" >> "$tracefile" 187 unset stopwatch 188 fi 189 } 190 191 getprevious() { 192 sed -i '$d' "$histfile" 193 prev="$(tail -n 1 "$histfile")" 194 sed -i '$d' "$histfile" 195 echo "$prev" 196 } 197 198 # Returns the complete url scheme with gemini defaults 199 # Parameters: url 200 parseurl() { 201 # Credits: https://stackoverflow.com/a/6174447/7618649 202 debug "parsing: $url oldhost: $oldhost oldpath: $oldpath" 203 proto="$(echo "$url" | grep :// | sed -e 's,^\(.*://\).*,\1,g')" 204 if [ "$proto" ] 205 then 206 url="$(echo "$url" | sed -e "s@$proto@@g")" 207 else 208 if [ "$oldhost" ] 209 then 210 case "$url" in 211 "/"*) url="$oldhost$url" ;; 212 *) oldpath="/${oldpath#/*}"; url="$oldhost${oldpath%/*}/$url" ;; 213 esac 214 fi 215 fi 216 debug "url: $url" 217 218 proto="$(echo "$proto" | sed -e 's,:\?//,,g')" 219 user="$(echo "$url" | grep @ | cut -d@ -f1)" 220 hostport="$(echo "$url" | sed -e "s/$user@//g" | cut -d/ -f1)" 221 host="$(echo "$hostport" | sed -e 's,:.*,,g')" 222 port="$(echo "$hostport" | sed -e 's,^.*:,:,g' -e 's,.*:\([0-9]*\).*,\1,g' -e 's,[^0-9],,g')" 223 path="$(echo "${url#/*}" | sed "s@/\?$hostport@@")" 224 225 debug "parsed: proto: ${proto:-gemini} host: $host port: ${port:-1965} path: ${path#/*}" 226 echo "${proto:-gemini}" "$host" "${port:-1965}" "${path#/*}" "$rest" 227 } 228 229 typesetgmi() { 230 # some setup first 231 [ -f "$linksfile" ] && rm "$linksfile" 232 cols=$(tput cols) 233 linkcount="0" 234 235 # shellcheck disable=SC2154 236 width=$((cols - (2*margin))) 237 debug "text width: $width" 238 239 stop 240 while IFS='' read -r line || [ -n "$line" ]; 241 do 242 # shellcheck disable=SC2059 243 line="$(printf "$line\n" | tr -d '\r')" 244 printf "$line\n" | grep -q "^\`\`\`" && pre=$((1 - pre)) && line="" 245 246 # add margins and fold 247 if [ "$pre" = 1 ] 248 then 249 printf '%*s%s\n' "$margin" "" "$line" 250 continue 251 fi 252 253 # shellcheck disable=SC2154 254 case "$line" in 255 "### "*) sty="$sty_header3" && line="${line#'### '}" ;; 256 "## "*) sty="$sty_header2" && line="${line#'## '}" ;; 257 "# "*) sty="$sty_header1" && line="${line#'# '}" ;; 258 "> "*) sty="$sty_quote" && line="${line#> }" ;; 259 "=>"*) 260 link="$(echo "${line#'=>'}" | tr -s '\t' ' ')" 261 echo "${link#' '}" >> "$linksfile" 262 linkcount=$((linkcount+1)) 263 264 line="$(echo "$link" | cut -d' ' -f2-)" 265 [ -z "$line" ] && line="$link" 266 267 sty="$sty_linkb${linkcount}$sty_linkt" 268 ;; 269 '* '*) sty="$sty_listb$sty_listt" && line="${line#* }";; 270 *) sty='' ;; 271 esac 272 273 # shellcheck disable=SC2059 274 printf -- "$line\n" | fold -w "$width" -s | while IFS='' read -r txt 275 do 276 printf "%*s" "$margin" "" 277 278 # shellcheck disable=SC2059 279 printf -- "$sty$txt\n\033[m" 280 done 281 done 282 stop "typeset" 283 } 284 285 # some help: 286 # \033[;H move to top 287 # \033[NH move to bottom N lines 288 # \033[?25l hide cursor 289 # \033[?25h unhide cursor 290 # \033[2K erase line 291 pager() { 292 clear 293 294 # hide cursor 295 printf '\033[?25l' 296 297 # lines columns 298 lines="$(tput lines)" 299 head -n "$((lines-1))" "$1" 300 l="$(wc -l < "$1")" 301 if [ "$lines" -lt "$l" ]; then pos="$lines"; else pos="$l"; fi 302 303 # stop echoing user input and read input unbufered 304 stty -echo -icanon min 1 time 0 305 306 # read inputs 307 while k="$(dd bs=1 count=1 status=none | od -c -An | tr -d ' ')" 308 do 309 case "$k" in 310 # command sequences 311 "033") 312 b="$(dd bs=2 count=1 status=none)" 313 case "$b" in 314 # up arrow 315 '[A') 316 [ "$pos" -le "$lines" ] && continue 317 line="$(sed "$((pos-lines))q;d" "$1")" 318 pos="$((pos-1))" 319 printf '\033[H\033[L%s\033[%sH\033[2K' "$line" "$lines" 320 ;; 321 # down arrow 322 '[B') 323 [ "$pos" -ge "$l" ] && continue 324 printf '\033[%sH' "$lines" 325 sed "${pos}q;d" "$1" 326 pos="$((pos+1))" 327 ;; 328 # page up 329 '[5') 330 # discard one extra byte 331 dd bs=1 count=1 status=none > /dev/null 332 333 [ "$pos" -le "$lines" ] && continue 334 scroll="$((pos-lines))" 335 [ "$scroll" -gt "$lines" ] && scroll="$((lines-1))" 336 337 # shellcheck disable=SC2086 338 for i in $(seq 1 "$scroll") 339 do 340 line="$(sed "$((pos-lines))q;d" "$1")" 341 pos="$((pos-1))" 342 printf '\033[H\033[L%s\033[%sH\033[2K' "$line" "$lines" 343 done 344 ;; 345 # page down 346 '[6') 347 # discard one extra byte 348 dd bs=1 count=1 status=none > /dev/null 349 350 [ "$pos" -ge "$l" ] && continue 351 scroll="$((lines-1))" 352 end="$((pos+scroll))" 353 [ "$end" -ge "$l" ] && scroll="$((l-pos))" 354 355 # shellcheck disable=SC2086 356 for i in $(seq 1 "$scroll") 357 do 358 printf '\033[%sH' "$lines" 359 sed "${pos}q;d" "$1" 360 pos="$((pos+1))" 361 done 362 ;; 363 esac ;; 364 "$quitkey") exit 0 ;; 365 "$openkey") 366 printf '\033[%sH\033[?25h\033[2KType url: ' "$lines" 367 stty echo icanon 368 read -r url <&1 369 370 # add gemini:// to begining if not (for convenience) 371 case "$url" in 372 "gemini://"*) ;; 373 *) url="gemini://$url" ;; 374 esac 375 return 376 ;; 377 "$openlocalkey") 378 # Open local gmi file 379 printf '\033[?25h\033[2KType file path: ' 380 stty echo 381 read -r file <&1 382 typesetgmi < "$file" > "$pagefile" 383 pager "$pagefile" 384 return 385 ;; 386 "$refreshkey") return ;; 387 "$gokey") 388 printf '\033[?25h\033[2KEnter link number: ' 389 stty echo icanon 390 read -r i <&1 391 debug "selected $i" 392 url="$(sed "${i}q;d" "$linksfile" | cut -f1 | cut -d' ' -f1)" 393 return 394 ;; 395 "$backkey") 396 read -r proto host port path <<- EOF 397 $(getprevious) 398 EOF 399 url="$proto://$host:$port/$path" 400 return 401 ;; 402 "$homekey") url="$homepage"; return ;; 403 "$markkey") 404 printf '\033[?25h\033[KEnter description: (optional)' 405 stty echo icanon 406 read -r desc <&1 407 echo "$url $desc" >> "$bookmarkfile" 408 return 409 ;; 410 "$gomarkkey") 411 clear 412 cat -n "$bookmarkfile" 413 printf "\033[?25h\033[KEnter link number: " 414 stty echo icanon 415 read -r i <&1 416 url="$(sed "${i}q;d" "$bookmarkfile" | cut -d' ' -f1)" 417 return 418 ;; 419 "$delmarkkey") 420 grep -iv "^$url " "$bookmarkfile" > "$cachedir/bookmarks" 421 mv "$cachedir/bookmarks" "$bookmarkfile" 422 return 423 ;; 424 "$goupkey") 425 newpath=$(echo "$url" | rev | cut -d'/' -f2- | rev) 426 url="${url%/*}/$newpath" 427 return 428 ;; 429 esac 430 done 431 } 432 433 # borrowed from https://gist.github.com/cdown/1163649 434 urlencode() { 435 stop 436 old_lang=$LANG 437 LANG=C 438 439 old_lc_collate=$LC_COLLATE 440 LC_COLLATE=C 441 442 length="${#1}" 443 i=1 444 while [ "$i" -le "$length" ] 445 do 446 c=$(printf '%s' "$1" | cut -c $i) 447 case $c in 448 [a-zA-Z0-9.~_-]) printf '%s' "$c" ;; 449 *) printf '%%%02X' "'$c" ;; 450 esac 451 i=$((i+1)) 452 done 453 454 LC_COLLATE=$old_lc_collate 455 LANG=$old_lang 456 stop "urlencode" 457 } 458 459 # Fetches the gemini response from server 460 # Parameters: proto, host, port and path 461 # Spec draft is here: https://gemini.circumlunar.space/docs/specification.html 462 fetch() { 463 if [ ! "$1" = "gemini" ] 464 then 465 echo "Only gemini links are supported." 466 echo "Type a key to continue." 467 read -r i <&1 468 read -r proto host port path <<- EOF 469 $(getprevious) 470 EOF 471 url="$proto://$host:$port/$path" 472 url="${url:-$homepage}" 473 debug "previous page: $url" 474 return 475 fi 476 477 debug "requesting $1://$2:$3/$4$5" 478 479 # set title 480 printf '\033]2;%s\007' "astro (c): $2/$4" 481 482 echo "$1 $2 $3 $4 $5" >> "$histfile" 483 484 certfile="" 485 if [ -f "$certdir/$2.crt" ] && [ -f "$certdir/$2.key" ] 486 then 487 certfile="-cert \"$certdir/$2.crt\" -key \"$certdir/$2.key\"" 488 debug "using client cert for domain: $certfile" 489 fi 490 491 port=$( [ "$3" = "1965" ] || ":$3" ) 492 url="$1://$2$port/$4$5" 493 [ "$trace" ] && echo "url: $url" >> "$tracefile" 494 495 stop 496 echo "$url" | eval openssl s_client \ 497 -connect "$2:$3" "$certfile" -crlf -quiet \ 498 -ign_eof 2> /dev/null > "$pagefile" 499 stop "openssl fetch" 500 stop 501 502 printf '\033]2;%s\007' "astro (r): $2/$4" 503 504 # First line is status and meta information 505 read -r status meta < "$pagefile" 506 status="$(echo "$status" | tr -d '\r\n')" 507 meta="$(echo "$meta" | tr -d '\r\n')" 508 sed -i '1d' "$pagefile" 509 stop "status extract" 510 debug "response status - meta: $status - $meta" 511 512 # Validate 513 case "$status" in 514 10) 515 echo "Input needed: $meta" >&2 516 echo "Please provide the input:" >&2 517 read -r input <&1 518 url="$1://$2:$3/$4?$(urlencode "$input")" 519 return 0 520 ;; 521 11) 522 echo "Sensitive input needed: $meta" >&2 523 read -r input <&1 524 url="$1://$2:$3/$4?$(urlencode "$input")" 525 return 0 526 ;; 527 30|31) 528 # Redirect 529 debug "redirecting to: $meta" 530 531 # shellcheck disable=SC2046 532 read -r proto host port path <<- EOF 533 $(oldhost="$2" oldpath="$4" url="$meta" parseurl) 534 EOF 535 url="$proto://$host:$port/$path" 536 return 0 537 ;; 538 40) 539 echo "Temporary failure" >&2 540 echo "Type a key to continue. " 541 read -r i <&1 542 return 3 543 ;; 544 41) 545 return 4 546 ;; 547 42) 548 return 5 549 ;; 550 43) 551 return 6 552 ;; 553 44) 554 return 7 555 ;; 556 50|51) 557 echo "Page not found!" >&2 558 echo "Type a key to return to previous page." 559 read -r i <&1 560 read -r proto host port path <<- EOF 561 $(getprevious) 562 EOF 563 url="$proto://$host:$port/$path" 564 debug "previous page: $url" 565 return 0 566 ;; 567 52) 568 return 10 569 ;; 570 53) 571 return 11 572 ;; 573 59) 574 echo "Bad request: $meta" >&2 575 echo "Type a key to continue." 576 read -r i <&1 577 return 12 578 ;; 579 60) 580 printf "client certificate required, to create a client cert use the following command:\n\n" 581 printf "\topenssl req -x509 -newkey rsa:4096 -keyout %s/%s.key -out %s/%s.crt -days 36500 -nodes\n\n" "$certdir" "$2" "$certdir" "$2" 582 printf "press 'return' to reload the page or 'b' to go back to the previous page:\n" 583 read -r in <&1 584 if [ "$in" = "b" ] 585 then 586 read -r proto host port path <<- EOF 587 $(getprevious) 588 EOF 589 url="$proto://$host:$port/$path" 590 else 591 url="$1://$2:$3/$4?$5" 592 fi 593 return 0 594 ;; 595 61) 596 return 14 597 ;; 598 62) 599 return 15 600 ;; 601 esac 602 603 # Success 604 oldhost="$2" 605 oldpath="$4" 606 607 # Set charset 608 charset="$(echo "$meta" | grep -i "charset=" | sed -e 's/.*charset=\([^;]\+\).*/\1/Ig')" 609 case "$charset" in 610 "iso-8859-1" | "ISO-8859-1") charset="iso8859" ;; 611 "utf-8" | "UTF-8" | "") charset="utf8" ;; 612 "us-ascii" | "US-ASCII") charset="ascii" ;; 613 esac 614 debug "charset: $charset" 615 616 printf '\033]2;%s\007' "astro (t): $2/$4" 617 case $meta in 618 "text/gemini"* | "") typesetgmi < "$pagefile" > "$pagefile.gmi"; mv "$pagefile.gmi" "$pagefile" ;; 619 *) ;; 620 esac 621 622 debug "starting pager" 623 printf '\033]2;%s\007' "astro (l): $2/$4" 624 pager "$pagefile" 625 626 debug "new url: $url" 627 } 628 629 # files are handled differently 630 if [ "$file" ] && [ -f "$file" ] 631 then 632 typesetgmi < "$file" > "$pagefile" 633 pager "$pagefile" 634 fi 635 636 # first request 637 url="${url:-$homepage}" 638 while : 639 do 640 printf '\033]2;%s\007' "astro (w)" 641 642 # shellcheck disable=SC2046 643 fetch $(parseurl) 644 done 645