commit fa011019579f2e7873048a74c4b41dfdf213f202
parent 2cb53c9535ca35d9e0e879c18f6b69adb531c4af
Author: William Casarin <jb55@jb55.com>
Date: Sat, 19 Sep 2020 05:16:04 -0700
bin: get everything up to sync
Diffstat:
16 files changed, 502 insertions(+), 3 deletions(-)
diff --git a/bin/.gitignore b/bin/.gitignore
@@ -12,3 +12,5 @@
/sacc
/otsclear
/txtnish
+/zebra
+/zoom-link-opener
diff --git a/bin/agena b/bin/agena
@@ -0,0 +1,252 @@
+#!/usr/bin/env python3
+
+import argparse
+import mimetypes
+import os
+import shlex
+import subprocess
+import socket
+import socketserver
+import ssl
+import sys
+import tempfile
+import urllib.parse
+
+try:
+ import chardet
+ _HAS_CHARDET = True
+except ImportError:
+ _HAS_CHARDET = False
+
+HOST, PORT = "0.0.0.0", 1965
+
+class AgenaHandler(socketserver.BaseRequestHandler):
+
+ def setup(self):
+ """
+ Wrap socket in SSL session.
+ """
+ self.request = context.wrap_socket(self.request, server_side=True)
+
+ def handle(self):
+ # Parse request URL, make sure it's for a Gopher resource
+ self.parse_request()
+ if self.request_scheme != "gopher":
+ self.send_gemini_header(50, "Agena only proxies to gopher resources.")
+ return
+ # Try to do a Gopher transaction with the remote host
+ try:
+ filename = self.download_gopher_resource()
+ except UnicodeError:
+ self.send_gemini_header(43, "Remote gopher host served content in an unrecognisable character encoding.")
+ return
+ except:
+ self.send_gemini_header(43, "Couldn't connect to remote gopher host.")
+ return
+ # Handle what we received based on item type
+ if self.gopher_itemtype == "0":
+ self.handle_text(filename)
+ elif self.gopher_itemtype == "1":
+ self.handle_menu(filename)
+ elif self.gopher_itemtype == "h":
+ self.handle_html(filename)
+ elif self.gopher_itemtype in ("9", "g", "I", "s"):
+ self.handle_binary(filename)
+ # Clean up
+ self.request.close()
+ os.unlink(filename)
+
+ def send_gemini_header(self, status, meta):
+ """
+ Send a Gemini header, and close the connection if the status code does
+ not indicate success.
+ """
+ self.request.send("{} {}\r\n".format(status, meta).encode("UTF-8"))
+ if status / 10 != 2:
+ self.request.close()
+
+ def parse_request(self):
+ """
+ Read a URL from the Gemini client and parse it up into parts,
+ including separating out the Gopher item type.
+ """
+ requested_url = self.request.recv(1024).decode("UTF-8").strip()
+ if "://" not in requested_url:
+ requested_url = "gemini://" + requested_url
+ parsed = urllib.parse.urlparse(requested_url)
+ self.request_scheme = parsed.scheme
+ self.gopher_host = parsed.hostname
+ self.gopher_port = parsed.port or 70
+ if parsed.path and parsed.path[0] == '/' and len(parsed.path) > 1:
+ self.gopher_itemtype = parsed.path[1]
+ self.gopher_selector = parsed.path[2:]
+ else:
+ # Use item type 1 for top-level selector
+ self.gopher_itemtype = "1"
+ self.gopher_selector = parsed.path
+ self.gopher_query = parsed.query
+
+ def download_gopher_resource(self):
+ """
+ Download the requested Gopher resource to a temporary file.
+ """
+ print("Requesting {} from {}...".format(self.gopher_selector, self.gopher_host), end="")
+
+ # Send request and read response
+ s = socket.create_connection((self.gopher_host, self.gopher_port))
+ if self.gopher_query:
+ request = self.gopher_selector + '\t' + self.gopher_query
+ else:
+ request = self.gopher_selector
+ request += '\r\n'
+ s.sendall(request.encode("UTF-8"))
+ response= s.makefile("rb").read()
+
+ # Transcode text responses into UTF-8
+ if self.gopher_itemtype in ("0", "1", "h"):
+ # Try some common encodings
+ for encoding in ("UTF-8", "ISO-8859-1"):
+ try:
+ response = response.decode("UTF-8")
+ break
+ except UnicodeDecodeError:
+ pass
+ else:
+ # If we didn't break out of the loop above, none of the
+ # common encodings worked. If we have chardet installed,
+ # try to autodetect.
+ if _HAS_CHARDET:
+ detected = chardet.detect(response)
+ response = response.decode(detected["encoding"])
+ else:
+ # Surrender.
+ raise UnicodeDecodeError
+ # Re-encode as God-fearing UTF-8
+ response = response.encode("UTF-8")
+
+ # Write gopher response to temp file
+ tmpf = tempfile.NamedTemporaryFile("wb", delete=False)
+ size = tmpf.write(response)
+ tmpf.close()
+ print("wrote {} bytes to {}...".format(size, tmpf.name))
+ return tmpf.name
+
+ def handle_text(self, filename):
+ """
+ Send a Gemini response for a downloaded Gopher resource whose item
+ type indicates it should be plain text.
+ """
+ self._serve_file("text/plain", filename)
+
+ def handle_menu(self, filename):
+ """
+ Send a Gemini response for a downloaded Gopher resource whose item
+ type indicates it should be a menu.
+ """
+ self.send_gemini_header(20, "text/gemini")
+ with open(filename,"r") as fp:
+ for line in fp:
+ if line.strip() == ".":
+ continue
+ elif line.startswith("i"):
+ # This is an "info" line. Just strip off the item type
+ # and send the item name, ignorin the dummy selector, etc.
+ self.request.send((line[1:].split("\t")[0]+"\r\n").encode("UTF-8"))
+ else:
+ # This is an actual link to a Gopher resource
+ gemini_link = self.gopher_link_to_gemini_link(line)
+ self.request.send(gemini_link.encode("UTF-8"))
+
+ def gopher_link_to_gemini_link(self, line):
+ """
+ Convert one line of a Gopher menu to one line of a Geminimap.
+ """
+
+ # Code below pinched from VF-1
+
+ # Split on tabs. Strip final element after splitting,
+ # since if we split first we loose empty elements.
+ parts = line.split("\t")
+ parts[-1] = parts[-1].strip()
+ # Discard Gopher+ noise
+ if parts[-1] == "+":
+ parts = parts[:-1]
+
+ # Attempt to assign variables. This may fail.
+ # It's up to the caller to catch the Exception.
+ name, path, host, port = parts
+ itemtype = name[0]
+ name = name[1:]
+ port = int(port)
+ if itemtype == "h" and path.startswith("URL:"):
+ url = path[4:]
+ else:
+ url = "gopher://%s%s/%s%s" % (
+ host,
+ "" if port == 70 else ":%d" % port,
+ itemtype,
+ path
+ )
+
+ return "=> {} {}\r\n".format(url, name)
+
+ def handle_html(self, filename):
+ """
+ Send a Gemini response for a downloaded Gopher resource whose item
+ type indicates it should be HTML.
+ """
+ self._serve_file("text/html", filename)
+
+ def handle_binary(self, filename):
+ """
+ Send a Gemini response for a downloaded Gopher resource whose item
+ type indicates it should be a binary file. Uses file(1) to sniff MIME
+ types.
+ """
+ # Detect MIME type
+ out = subprocess.check_output(
+ shlex.split("file --brief --mime-type %s" % filename)
+ )
+ mimetype = out.decode("UTF-8").strip()
+ self._serve_file(mimetype, filename)
+
+ def _serve_file(self, mime, filename):
+ """
+ Send a Gemini response with a given MIME type whose body is the
+ contents of the specified file.
+ """
+ self.send_gemini_header(20, mime)
+ with open(filename,"rb") as fp:
+ self.request.send(fp.read())
+
+if __name__ == "__main__":
+
+ parser = argparse.ArgumentParser(description=
+"""Agena is a simple Gemini-to-Gopher designed to be run locally to
+let you seamlessly access Gopherspace from inside a Gemini client.""")
+ parser.add_argument('--cert', type=str, nargs="?", default="cert.pem",
+ help='TLS certificate file.')
+ parser.add_argument('--key', type=str, nargs="?", default="key.pem",
+ help='TLS private key file.')
+ parser.add_argument('--port', type=int, nargs="?", default=PORT,
+ help='TCP port to serve on.')
+ parser.add_argument('--host', type=str, nargs="?", default=HOST,
+ help='TCP host to serve on.')
+ args = parser.parse_args()
+ print(args)
+
+ if not (os.path.exists(args.cert) and os.path.exists(args.key)):
+ print("Couldn't find cert.pem and/or key.pem. :(")
+ sys.exit(1)
+
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ context.load_cert_chain(certfile=args.cert, keyfile=args.key)
+
+ socketserver.TCPServer.allow_reuse_address = True
+ agena = socketserver.TCPServer((args.host, args.port), AgenaHandler)
+ try:
+ agena.serve_forever()
+ except KeyboardInterrupt:
+ agena.shutdown()
+ agena.server_close()
+
diff --git a/bin/arcsize b/bin/arcsize
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+cat /proc/spl/kstat/zfs/arcstats | grep -E ^size | awk '{print $3}'
diff --git a/bin/book b/bin/book
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+BOOKS_DIR="$HOME/docs/books"
+
+find "$BOOKS_DIR" -name '*.txt' -type f -printf "%f\n" |
+ fzf |
+ xargs -I{} lessr $BOOKS_DIR/{}
diff --git a/bin/btc-txs-raw b/bin/btc-txs-raw
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+WALLETS=${WALLETS:-$(bcli listwallets | jq -r '.[]' | paste -sd" " )}
+
+(for wallet in $WALLETS
+do
+ bcli -rpcwallet="$wallet" "$@" listtransactions '*' 2000 \
+ | jq -rc '.[] | {label: .label, address: .address, category: .category, amount: .amount, blocktime: .blocktime}'
+done) \
+ | jq -src 'sort_by(.time) | .[] | [.label,.address,.category,.amount,(.blocktime | strftime("%F %R"))] | @tsv'
+
diff --git a/bin/gemini b/bin/gemini
@@ -1,2 +1,2 @@
#!/usr/bin/env sh
-exec reader $GEMINICLIENT "$@"
+exec $GEMINICLIENT "$@"
diff --git a/bin/killspotify b/bin/killspotify
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+ps ax | grep spotify-wrapped | grep -v grep | head -n1 | awk '{print $1}' | xargs kill
diff --git a/bin/lessr b/bin/lessr
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+dir=$(dirname "$1")
+base=$(basename "$1")
+
+export LESSHISTFILE="$dir/.$base.hst"
+
+exec less "$@"
diff --git a/bin/lockmac b/bin/lockmac
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+ip=$(macip)
+
+ssh "$ip" "osascript -e 'tell application \"System Events\" to keystroke \"q\" using {control down, command down}'"
diff --git a/bin/macip b/bin/macip
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+
+exec arp -a -i enp30s0 | grep -e '40:6c:8f:39:43:e9' -e 'c4:41:1e:75:42:71' | sedcut '(\([0-9\.]\+\))'
+
diff --git a/bin/notmuch-emacs-mua b/bin/notmuch-emacs-mua
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+#
+# notmuch-emacs-mua - start composing a mail on the command line
+#
+# Copyright © 2014 Jani Nikula
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see https://www.gnu.org/licenses/ .
+#
+# Authors: Jani Nikula <jani@nikula.org>
+#
+
+set -eu
+
+# escape: "expand" '\' as '\\' and '"' as '\"'
+# calling convention: escape -v var "$arg" (like in bash printf).
+escape ()
+{
+ local __escape_arg__=${3//\\/\\\\}
+ printf -v $2 '%s' "${__escape_arg__//\"/\\\"}"
+}
+
+EMACS=${EMACS:-emacs}
+EMACSCLIENT=${EMACSCLIENT:-emacsclient}
+
+PRINT_ONLY=
+NO_WINDOW=
+USE_EMACSCLIENT=
+AUTO_DAEMON=
+CREATE_FRAME=
+ELISP=
+MAILTO=
+HELLO=
+
+# Short options compatible with mutt(1).
+while getopts :s:c:b:i:h opt; do
+ # Handle errors and long options.
+ case "${opt}" in
+ :)
+ echo "$0: short option -${OPTARG} requires an argument." >&2
+ exit 1
+ ;;
+ \?)
+ opt=$1
+ if [ "${OPTARG}" != "-" ]; then
+ echo "$0: unknown short option -${OPTARG}." >&2
+ exit 1
+ fi
+
+ case "${opt}" in
+ # Long options with arguments.
+ --subject=*|--to=*|--cc=*|--bcc=*|--body=*)
+ OPTARG=${opt#--*=}
+ opt=${opt%%=*}
+ ;;
+ # Long options without arguments.
+ --help|--print|--no-window-system|--client|--auto-daemon|--create-frame|--hello)
+ ;;
+ *)
+ echo "$0: unknown long option ${opt}, or argument mismatch." >&2
+ exit 1
+ ;;
+ esac
+ # getopts does not do this for what it considers errors.
+ OPTIND=$((OPTIND + 1))
+ ;;
+ esac
+
+ escape -v OPTARG "${OPTARG-none}"
+
+ case "${opt}" in
+ --help|h)
+ exec man notmuch-emacs-mua
+ ;;
+ --subject|s)
+ ELISP="${ELISP} (message-goto-subject) (insert \"${OPTARG}\")"
+ ;;
+ --to)
+ ELISP="${ELISP} (message-goto-to) (insert \"${OPTARG}, \")"
+ ;;
+ --cc|c)
+ ELISP="${ELISP} (message-goto-cc) (insert \"${OPTARG}, \")"
+ ;;
+ --bcc|b)
+ ELISP="${ELISP} (message-goto-bcc) (insert \"${OPTARG}, \")"
+ ;;
+ --body|i)
+ ELISP="${ELISP} (message-goto-body) (insert-file \"${OPTARG}\")"
+ ;;
+ --print)
+ PRINT_ONLY=1
+ ;;
+ --no-window-system)
+ NO_WINDOW="-nw"
+ ;;
+ --client)
+ USE_EMACSCLIENT="yes"
+ ;;
+ --auto-daemon)
+ AUTO_DAEMON="--alternate-editor="
+ CREATE_FRAME="-c"
+ ;;
+ --create-frame)
+ CREATE_FRAME="-c"
+ ;;
+ --hello)
+ HELLO=1
+ ;;
+ *)
+ # We should never end up here.
+ echo "$0: internal error (option ${opt})." >&2
+ exit 1
+ ;;
+ esac
+
+ shift $((OPTIND - 1))
+ OPTIND=1
+done
+
+# Positional parameters.
+for arg; do
+ escape -v arg "${arg}"
+ case $arg in
+ mailto:*)
+ if [ -n "${MAILTO}" ]; then
+ echo "$0: more than one mailto: argument." >&2
+ exit 1
+ fi
+ MAILTO="${arg}"
+ ;;
+ *)
+ ELISP="${ELISP} (message-goto-to) (insert \"${arg}, \")"
+ ;;
+ esac
+done
+
+if [ -n "${MAILTO}" ]; then
+ if [ -n "${ELISP}" ]; then
+ echo "$0: mailto: is not compatible with other message parameters." >&2
+ exit 1
+ fi
+ ELISP="(browse-url-mail \"${MAILTO}\")"
+elif [ -z "${ELISP}" -a -n "${HELLO}" ]; then
+ ELISP="(notmuch)"
+else
+ ELISP="(notmuch-mua-new-mail) ${ELISP}"
+fi
+
+# Kill the terminal/frame if we're creating one.
+if [ -z "$USE_EMACSCLIENT" -o -n "$CREATE_FRAME" -o -n "$NO_WINDOW" ]; then
+ ELISP="${ELISP} (message-add-action #'save-buffers-kill-terminal 'exit)"
+fi
+
+escape -v pwd "$PWD"
+
+# The crux of it all: construct an elisp progn and eval it.
+ELISP="(prog1 'done (require 'notmuch) (cd \"$pwd\") ${ELISP})"
+
+if [ -n "$PRINT_ONLY" ]; then
+ echo ${ELISP}
+ exit 0
+fi
+
+if [ -n "$USE_EMACSCLIENT" ]; then
+ # Evaluate the progn.
+ exec ${EMACSCLIENT} ${NO_WINDOW} ${CREATE_FRAME} ${AUTO_DAEMON} --eval "${ELISP}"
+else
+ exec ${EMACS} ${NO_WINDOW} --eval "${ELISP}"
+fi
diff --git a/bin/paged b/bin/paged
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec "$@" | less -R
diff --git a/bin/pdfsave b/bin/pdfsave
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+#PANDOC='pandoc --metadata=fontfamily:libertine'
+PANDOC='pandoc --pdf-engine=xelatex --metadata=fontfamily:libertine -V geometry:margin=1.2in --variable urlcolor=cyan'
+#PANDOC='pandoc -V geometry:margin=1.2in --variable urlcolor=cyan'
+
+usage () {
+ printf "usage: pdfsave file.md out.pdf OR <file.md pdfsave out.pdf [markdown]\n" >&2
+ exit 1
+}
+
+if [ -t 0 ]; then
+ [ $# -eq 0 ] && usage
+ outfile=${2:-"$(mktemp)".pdf}
+ $PANDOC "$1" -o "$outfile"
+else
+ outfile=${1:-"$(mktemp)".pdf}
+ ext="${2:-markdown}"
+ $PANDOC -f "$ext" -o "$outfile"
+fi
diff --git a/bin/procmem b/bin/procmem
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+ps -eo pmem,rss,comm |
+ grep -v '\[' |
+ awk 'NR>2{mem[$3]+=$2*1024}END {for(k in mem) print mem[k] "\t" k};' |
+ sort -gk 1 |
+ column -t -s $'\t'
diff --git a/bin/procmemall b/bin/procmemall
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+procmem | awk '{print $2}' | paste -sd+ | bc
diff --git a/bin/reader b/bin/reader
@@ -1,2 +0,0 @@
-#!/usr/bin/env bash
-exec urxvtc -fn 'xft:Inconsolata:size=18' -e "$@"