#!/bin/sh version="0.22.4" usage() { echo "astro v$version: Browse the gemini web on the terminal." echo "" echo "Usage: astro [url]|[option]" echo "" echo "Options:" echo " -h, --help show this help" echo " -v, --version show version info" echo "" echo "Commands available inside the browser:" echo " q quit" echo " g go to a link" echo " r reload current page" echo " b go back one page" echo " u jump one path segment up" echo " o open an address" echo " s save current page" echo " H go to homepage" echo " m add bookmark" echo " M go to a bookmark" echo " K remove bookmark for current url" echo "" echo "Examples:" echo " astro Start browsing the default webpage" echo " astro url Start browsing url" echo " astro --help Show help" echo "" echo "Debugging:" echo " debug=1 astro Will start astro in debug mode" echo "" echo "Report bugs to: bleemayer@gmail.com" echo "Home page: " echo "General help: " } version() { echo "astro $version" echo "Copyright (C) 2021-2023 Brian Mayer." echo "License MIT: MIT License " echo "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND," echo "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF" echo "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT." echo "" echo "Written by Brian Lee Mayer." } debug() { [ "$debug" ] && echo "DEBUG: $*" >&2 && sleep "$debug" } # Parse arguments args="$*" case "$args" in '--help'* | '-h'*) usage exit ;; '--version'* | '-v'*) version exit ;; esac # Save terminal tput smcup # Configuration confighome=${XDG_CONFIG_HOME:-$HOME/.config} mkdir -p "$confighome/astro" configfile="$confighome/astro/astro.conf" bookmarkfile="$confighome/astro/bookmarks" LESSKEY="$confighome/astro/less.keys" certdir="$confighome/astro/certs" mkdir -p "$certdir" cachedir="${XDG_CACHE_HOME:-$HOME/.cache}/astro" # This is the final binary form, to save space, it corresponds to: # o (49): go to a URL # r (50): reload page # g (51): go to a link # b (52): go back # H (53): go to homepage # m (54): add page to bookmarks # M (55): go to a bookmark # K (56): remove bookmark for current url # u (57): jump one path element up [ -n "$LESSKEY" ] && echo "AE0rR2MtAG8AmDEAcgCYMgBnAJgzAGIAmDQASACYNQBtAJg2AE0AmDcASwCYOAB1AJg5AGUAAHYAAHhFbmQ=" | \ base64 -d > "$LESSKEY" if [ ! -s "$configfile" ] then # Default values cat << EOF > "$configfile" cachedir="$cachedir" margin=8 homepage="gemini.circumlunar.space/" sty_header1='\\033[35;7;1m' sty_header2='\\033[35;4;1m' sty_header3='\\033[35;4m' sty_quote='\\033[2;3m ' sty_linkb='\\033[35m' sty_linkt=' => \\033[36;3m ' sty_listb='\\033[35;1m •' sty_listt='\\033[0m ' EOF fi # shellcheck disable=SC1090 . "$configfile" mkdir -p "$cachedir" pagefile="$(mktemp -p "$cachedir" -t curpage.XXXXXX)" histfile="$(mktemp -p "$cachedir" -t history.XXXXXX)" linksfile="$(mktemp -p "$cachedir" -t links.XXXXXX)" tracefile="$(mktemp -p "$cachedir" -t trace.XXXXXX)" # Restore terminal trap 'tput rmcup && rm -f $histfile $linksfile $pagefile > /dev/null 2>&1; exit' EXIT INT HUP stop() { [ "$trace" ] || return if [ -z "$stopwatch" ] then stopwatch=$(date +%s.%N) else dur=$(echo "$(date +%s.%N) - $stopwatch" | bc) printf "%s took %s seconds\n" "$1" "$dur" >> "$tracefile" unset stopwatch fi } getprevious() { sed -i '$d' "$histfile" prev="$(tail -n 1 "$histfile")" sed -i '$d' "$histfile" echo "$prev" } # Returns the complete url scheme with gemini defaults # Parameters: url parseurl() { # Credits: https://stackoverflow.com/a/6174447/7618649 debug "parsing: $url oldhost: $oldhost oldpath: $oldpath" proto="$(echo "$url" | grep :// | sed -e 's,^\(.*://\).*,\1,g')" if [ "$proto" ] then url="$(echo "$url" | sed -e "s@$proto@@g")" else if [ "$oldhost" ] then case "$url" in "/"*) url="$oldhost$url" ;; *) oldpath="/${oldpath#/*}"; url="$oldhost${oldpath%/*}/$url" ;; esac fi fi debug "url: $url" proto="$(echo "$proto" | sed -e 's,:\?//,,g')" user="$(echo "$url" | grep @ | cut -d@ -f1)" hostport="$(echo "$url" | sed -e "s/$user@//g" | cut -d/ -f1)" host="$(echo "$hostport" | sed -e 's,:.*,,g')" port="$(echo "$hostport" | sed -e 's,^.*:,:,g' -e 's,.*:\([0-9]*\).*,\1,g' -e 's,[^0-9],,g')" path="$(echo "${url#/*}" | sed "s@/\?$hostport@@")" debug "parsed: proto: ${proto:-gemini} host: $host port: ${port:-1965} path: ${path#/*}" echo "${proto:-gemini}" "$host" "${port:-1965}" "${path#/*}" "$rest" } typesetgmi() { stop while IFS='' read -r line || [ -n "$line" ]; do line="$(echo "$line" | tr -d '\r')" # shellcheck disable=SC2016 echo "$line" | grep -q '^```' && pre=$((1 - pre)) && line="" # add margins and fold if [ "$pre" = 1 ] then # shellcheck disable=SC2154 printf '%*s%s\n' "$margin" "" "$line" continue fi # shellcheck disable=SC2154 case "$line" in "### "*) sty="$sty_header3" && line="${line#'### '}" ;; "## "*) sty="$sty_header2" && line="${line#'## '}" ;; "# "*) sty="$sty_header1" && line="${line#'# '}" ;; "> "*) sty="$sty_quote" && line="${line#> }" ;; "=>"*) link=${line#'=>'} echo "${link#' '}" >> "$linksfile" linkcount=$((linkcount+1)) line="$(echo "$link" | cut -d' ' -f2-)" [ -z "$line" ] && line="$link" sty="$sty_linkb${linkcount}$sty_linkt" ;; '* '*) sty="$sty_listb$sty_listt" && line="${line#* }";; *) sty="";; esac echo "$line" | fold -w "$width" -s | { while IFS='' read -r txt do printf "%*s" "$margin" "" # shellcheck disable=SC2059 printf "$sty" echo "$txt" done } done stop "typeset" } # borrowed from https://gist.github.com/cdown/1163649 urlencode() { stop old_lang=$LANG LANG=C old_lc_collate=$LC_COLLATE LC_COLLATE=C length="${#1}" i=1 while [ "$i" -le "$length" ] do c=$(printf '%s' "$1" | cut -c $i) case $c in [a-zA-Z0-9.~_-]) printf '%s' "$c" ;; *) printf '%%%02X' "'$c" ;; esac i=$((i+1)) done LC_COLLATE=$old_lc_collate LANG=$old_lang stop "urlencode" } # Fetches the gemini response from server # Parameters: proto, host, port and path # Spec draft is here: https://gemini.circumlunar.space/docs/specification.html fetch() { if [ ! "$1" = "gemini" ] then echo "Only gemini links are supported." echo "Type a key to continue." read -r i <&1 read -r proto host port path << EOF $(getprevious) EOF url="$proto://$host:$port/$path" url="${url:-$homepage}" debug "previous page: $url" return fi # Some setup first cols=$(tput cols) width=$((cols - (2*margin))) debug "text width: $width" debug "requesting $1://$2:$3/$4$5" printf '\033]2;%s\007' "astro: $2/$4" echo "$1 $2 $3 $4 $5" >> "$histfile" clear certfile="" if [ -f "$certdir/$2.crt" ] && [ -f "$certdir/$2.key" ] then certfile="-cert \"$certdir/$2.crt\" -key \"$certdir/$2.key\"" debug "using client cert for domain: $certfile" fi port=$( [ "$3" = "1965" ] || ":$3" ) url="$1://$2$port/$4$5" [ "$trace" ] && echo "url: $url" >> "$tracefile" stop echo "$url" | eval openssl s_client \ -connect "$2:$3" "$certfile" -crlf -quiet \ -ign_eof 2> /dev/null > "$pagefile" stop "openssl fetch" stop # First line is status and meta information read -r status meta < "$pagefile" status="$(echo "$status" | tr -d '\r\n')" meta="$(echo "$meta" | tr -d '\r\n')" sed -i '1d' "$pagefile" stop "status extract" debug "response status - meta: $status - $meta" # Validate case "$status" in 10) echo "Input needed: $meta" >&2 echo "Please provide the input:" >&2 read -r input <&1 url="$1://$2:$3/$4?$(urlencode "$input")" return 0 ;; 11) echo "Sensitive input needed: $meta" >&2 read -r input <&1 url="$1://$2:$3/$4?$(urlencode "$input")" return 0 ;; 30|31) # Redirect debug "redirecting to: $meta" # shellcheck disable=SC2046 read -r proto host port path << EOF $(oldhost="$2" oldpath="$4" url="$meta" parseurl) EOF url="$proto://$host:$port/$path" return 0 ;; 40) echo "Temporary failure" >&2 echo "Type a key to continue. " read -r i <&1 return 3 ;; 41) return 4 ;; 42) return 5 ;; 43) return 6 ;; 44) return 7 ;; 50|51) echo "Page not found!" >&2 echo "Type a key to return to previous page." read -r i <&1 read -r proto host port path << EOF $(getprevious) EOF url="$proto://$host:$port/$path" debug "previous page: $url" return 0 ;; 52) return 10 ;; 53) return 11 ;; 59) echo "Bad request: $meta" >&2 echo "Type a key to continue." read -r i <&1 return 12 ;; 60) printf "client certificate required, to create a client cert use the following command:\n\n" printf "\topenssl req -x509 -newkey rsa:4096 -keyout %s/%s.key -out %s/%s.crt -days 36500 -nodes\n\n" "$certdir" "$2" "$certdir" "$2" printf "press 'return' to reload the page or 'b' to go back to the previous page:\n" read -r in <&1 if [ "$in" = "b" ] then read -r proto host port path << EOF $(getprevious) EOF url="$proto://$host:$port/$path" else url="$1://$2:$3/$4?$5" fi return 0 ;; 61) return 14 ;; 62) return 15 ;; esac # Success [ -f "$linksfile" ] && rm "$linksfile" # Set charset charset="$(echo "$meta" | grep -i "charset=" | sed -e 's/.*charset=\([^;]\+\).*/\1/Ig')" case "$charset" in "iso-8859-1" | "ISO-8859-1") charset="iso8859" ;; "utf-8" | "UTF-8" | "") charset="utf8" ;; "us-ascii" | "US-ASCII") charset="ascii" ;; esac debug "charset: $charset" case $meta in "text/gemini"* | "") typesetgmi ;; *) cat ;; esac < "$pagefile" | LESSCHARSET="$charset" less -k "$LESSKEY" +k -R code="$?" rm "$pagefile" # Choose what to do next debug "pager exit code: $code" case "$code" in 0) exit 0 ;; 49) # Open url printf "Type url: " read -r url <&1 ;; 50) url="$1://$2:$3/$4" ;; 51) # Follow link cat -n "$linksfile" printf "Enter link number: " read -r i <&1 debug "selected $i" url="$(sed "${i}q;d" "$linksfile" | cut -f1 | cut -d' ' -f1)" oldhost="$2"; oldpath="$4" ;; 52) read -r proto host port path << EOF $(getprevious) EOF url="$proto://$host:$port/$path" ;; 53) url="$homepage"; shift $# ;; 54) echo "Enter description: (optional)" read -r desc <&1 url="$1://$2:$3/$4" echo "$url $desc" >> "$bookmarkfile" ;; 55) cat -n "$bookmarkfile" printf "Enter link number: " read -r i <&1 url="$(sed "${i}q;d" "$bookmarkfile" | cut -d' ' -f1)" ;; 56) url="$1://$2:$3/$4" grep -iv "^$url " "$bookmarkfile" > "$cachedir/bookmarks" mv "$cachedir/bookmarks" "$bookmarkfile" ;; 57) newpath=$(echo "/${4%/}" | rev | cut -d'/' -f2- | rev) url="$1://$2:$3$newpath" ;; esac debug "new url: $url" } # Execution export LESS='-P Keys\: qgrubosHmMK, to see a description run astro -h' # First request url="${args:-$homepage}" while : do # shellcheck disable=SC2046 fetch $(parseurl) done