commit 11e4ada074a4971339deb90a0b358879ab777c9b
parent 0babad5344527de0c554bcfb2e8266e45c3ca5cd
Author: William Casarin <jb55@jb55.com>
Date: Tue, 15 Sep 2020 20:57:58 -0700
add bin tree
Diffstat:
249 files changed, 5028 insertions(+), 0 deletions(-)
diff --git a/bin/.gitignore b/bin/.gitignore
@@ -0,0 +1,14 @@
+/bcalc
+/cmdtree
+/colorpicker
+/daedalus
+/logtop
+/ratio
+/dyalog
+/cardano
+/btcs
+/otsmini
+/otsprint
+/sacc
+/otsclear
+/txtnish
diff --git a/bin/.gitmodules b/bin/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "curlpaste"]
+ path = curlpaste
+ url = git://github.com/Kiwi/curlpaste.git
+[submodule "nix-config"]
+ path = nix-config
+ url = gh:jb55/nix-config
diff --git a/bin/all-dev b/bin/all-dev
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+export RUST_CHANNEL=stable
+#export RUST_SRC_PATH="$(nix-build '<nixpkgs>' --no-out-link -A rustChannels.$RUST_CHANNEL.rust-src)"/lib/rustlib/src/rust/src
+#export LD_LIBRARY_PATH="$(nix-build '<nixpkgs>' --no-out-link -A rustChannels.$RUST_CHANNEL.rustc)"/lib:$LD_LIBRARY_PATH
+
+exec nix-shell -p hello "$@"
diff --git a/bin/ariline b/bin/ariline
@@ -0,0 +1 @@
+tr \\r \\n
diff --git a/bin/av98 b/bin/av98
@@ -0,0 +1,1585 @@
+#!/usr/bin/env python3
+# AV-98 Gemini client
+# Dervied from VF-1 (https://github.com/solderpunk/VF-1),
+# (C) 2019, 2020 Solderpunk <solderpunk@sdf.org>
+# With contributions from:
+# - danceka <hannu.hartikainen@gmail.com>
+# - <jprjr@tilde.club>
+# - <vee@vnsf.xyz>
+# - Klaus Alexander Seistrup <klaus@seistrup.dk>
+
+import argparse
+import cmd
+import cgi
+import codecs
+import collections
+import datetime
+import fnmatch
+import getpass
+import glob
+import hashlib
+import io
+import mimetypes
+import os
+import os.path
+import random
+import shlex
+import shutil
+import socket
+import sqlite3
+import ssl
+from ssl import CertificateError
+import subprocess
+import sys
+import tempfile
+import time
+import urllib.parse
+import uuid
+import webbrowser
+
+try:
+ import ansiwrap as textwrap
+except ModuleNotFoundError:
+ import textwrap
+
+try:
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend
+ _HAS_CRYPTOGRAPHY = True
+ _BACKEND = default_backend()
+except ModuleNotFoundError:
+ _HAS_CRYPTOGRAPHY = False
+
+_VERSION = "1.0.2dev"
+
+_MAX_REDIRECTS = 5
+
+# Command abbreviations
+_ABBREVS = {
+ "a": "add",
+ "b": "back",
+ "bb": "blackbox",
+ "bm": "bookmarks",
+ "book": "bookmarks",
+ "f": "fold",
+ "fo": "forward",
+ "g": "go",
+ "h": "history",
+ "hist": "history",
+ "l": "less",
+ "n": "next",
+ "p": "previous",
+ "prev": "previous",
+ "q": "quit",
+ "r": "reload",
+ "s": "save",
+ "se": "search",
+ "/": "search",
+ "t": "tour",
+ "u": "up",
+}
+
+_MIME_HANDLERS = {
+ "application/pdf": "xpdf %s",
+ "audio/mpeg": "mpg123 %s",
+ "audio/ogg": "ogg123 %s",
+ "image/*": "feh %s",
+ "text/html": "lynx -dump -force_html %s",
+ "text/plain": "cat %s",
+ "text/gemini": "cat %s",
+}
+
+# monkey-patch Gemini support in urllib.parse
+# see https://github.com/python/cpython/blob/master/Lib/urllib/parse.py
+urllib.parse.uses_relative.append("gemini")
+urllib.parse.uses_netloc.append("gemini")
+
+
+def fix_ipv6_url(url):
+ if not url.count(":") > 2: # Best way to detect them?
+ return url
+ # If there's a pair of []s in there, it's probably fine as is.
+ if "[" in url and "]" in url:
+ return url
+ # Easiest case is a raw address, no schema, no path.
+ # Just wrap it in square brackets and whack a slash on the end
+ if "/" not in url:
+ return "[" + url + "]/"
+ # Now the trickier cases...
+ if "://" in url:
+ schema, schemaless = url.split("://")
+ else:
+ schema, schemaless = None, url
+ if "/" in schemaless:
+ netloc, rest = schemaless.split("/",1)
+ schemaless = "[" + netloc + "]" + "/" + rest
+ if schema:
+ return schema + "://" + schemaless
+ return schemaless
+
+standard_ports = {
+ "gemini": 1965,
+ "gopher": 70,
+}
+
+class GeminiItem():
+
+ def __init__(self, url, name=""):
+ if "://" not in url:
+ url = "gemini://" + url
+ self.url = fix_ipv6_url(url)
+ self.name = name
+ parsed = urllib.parse.urlparse(self.url)
+ self.scheme = parsed.scheme
+ self.host = parsed.hostname
+ self.port = parsed.port or standard_ports.get(self.scheme, 0)
+ self.path = parsed.path
+
+ def root(self):
+ return GeminiItem(self._derive_url("/"))
+
+ def up(self):
+ pathbits = list(os.path.split(self.path.rstrip('/')))
+ # Don't try to go higher than root
+ if len(pathbits) == 1:
+ return self
+ # Get rid of bottom component
+ pathbits.pop()
+ new_path = os.path.join(*pathbits)
+ return GeminiItem(self._derive_url(new_path))
+
+ def query(self, query):
+ query = urllib.parse.quote(query)
+ return GeminiItem(self._derive_url(query=query))
+
+ def _derive_url(self, path="", query=""):
+ """
+ A thin wrapper around urlunparse which avoids inserting standard ports
+ into URLs just to keep things clean.
+ """
+ return urllib.parse.urlunparse((self.scheme,
+ self.host if self.port == standard_ports[self.scheme] else self.host + ":" + str(self.port),
+ path or self.path, "", query, ""))
+
+ def absolutise_url(self, relative_url):
+ """
+ Convert a relative URL to an absolute URL by using the URL of this
+ GeminiItem as a base.
+ """
+ return urllib.parse.urljoin(self.url, relative_url)
+
+ def to_map_line(self, name=None):
+ if name or self.name:
+ return "=> {} {}\n".format(self.url, name or self.name)
+ else:
+ return "=> {}\n".format(self.url)
+
+ @classmethod
+ def from_map_line(cls, line, origin_gi):
+ assert line.startswith("=>")
+ assert line[2:].strip()
+ bits = line[2:].strip().split(maxsplit=1)
+ bits[0] = origin_gi.absolutise_url(bits[0])
+ return cls(*bits)
+
+CRLF = '\r\n'
+
+# Cheap and cheerful URL detector
+def looks_like_url(word):
+ return "." in word and word.startswith("gemini://")
+
+# GeminiClient Decorators
+def needs_gi(inner):
+ def outer(self, *args, **kwargs):
+ if not self.gi:
+ print("You need to 'go' somewhere, first")
+ return None
+ else:
+ return inner(self, *args, **kwargs)
+ outer.__doc__ = inner.__doc__
+ return outer
+
+def restricted(inner):
+ def outer(self, *args, **kwargs):
+ if self.restricted:
+ print("Sorry, this command is not available in restricted mode!")
+ return None
+ else:
+ return inner(self, *args, **kwargs)
+ outer.__doc__ = inner.__doc__
+ return outer
+
+class GeminiClient(cmd.Cmd):
+
+ def __init__(self, restricted=False):
+ cmd.Cmd.__init__(self)
+
+ # Set umask so that nothing we create can be read by anybody else.
+ # The certificate cache and TOFU database contain "browser history"
+ # type sensitivie information.
+ os.umask(0o077)
+
+ # Find config directory
+ ## Look for something pre-existing
+ for confdir in ("~/.av98/", "~/.config/av98/"):
+ confdir = os.path.expanduser(confdir)
+ if os.path.exists(confdir):
+ self.config_dir = confdir
+ break
+ ## Otherwise, make one in .config if it exists
+ else:
+ if os.path.exists(os.path.expanduser("~/.config/")):
+ self.config_dir = os.path.expanduser("~/.config/av98/")
+ else:
+ self.config_dir = os.path.expanduser("~/.av98/")
+ print("Creating config directory {}".format(self.config_dir))
+ os.makedirs(self.config_dir)
+
+ self.no_cert_prompt = "\x1b[38;5;76m" + "AV-98" + "\x1b[38;5;255m" + "> " + "\x1b[0m"
+ self.cert_prompt = "\x1b[38;5;202m" + "AV-98" + "\x1b[38;5;255m" + "+cert> " + "\x1b[0m"
+ self.prompt = self.no_cert_prompt
+ self.always_less = False
+ self.gi = None
+ self.history = []
+ self.hist_index = 0
+ self.idx_filename = ""
+ self.index = []
+ self.index_index = -1
+ self.lookup = self.index
+ self.marks = {}
+ self.page_index = 0
+ self.permanent_redirects = {}
+ self.previous_redirectors = set()
+ self.restricted = restricted
+ self.tmp_filename = ""
+ self.visited_hosts = set()
+ self.waypoints = []
+
+ self.client_certs = {
+ "active": None
+ }
+ self.active_cert_domains = []
+ self.active_is_transient = False
+ self.transient_certs_created = []
+
+ self.options = {
+ "debug" : False,
+ "ipv6" : True,
+ "timeout" : 10,
+ "width" : 80,
+ "auto_follow_redirects" : True,
+ "gopher_proxy" : None,
+ "tls_mode" : "tofu",
+ }
+
+ self.log = {
+ "start_time": time.time(),
+ "requests": 0,
+ "ipv4_requests": 0,
+ "ipv6_requests": 0,
+ "bytes_recvd": 0,
+ "ipv4_bytes_recvd": 0,
+ "ipv6_bytes_recvd": 0,
+ "dns_failures": 0,
+ "refused_connections": 0,
+ "reset_connections": 0,
+ "timeouts": 0,
+ }
+
+ self._connect_to_tofu_db()
+
+ def _connect_to_tofu_db(self):
+
+ db_path = os.path.join(self.config_dir, "tofu.db")
+ self.db_conn = sqlite3.connect(db_path)
+ self.db_cur = self.db_conn.cursor()
+
+ self.db_cur.execute("""CREATE TABLE IF NOT EXISTS cert_cache
+ (hostname text, address text, fingerprint text,
+ first_seen date, last_seen date, count integer)""")
+
+ def _go_to_gi(self, gi, update_hist=True, handle=True):
+ """This method might be considered "the heart of AV-98".
+ Everything involved in fetching a gemini resource happens here:
+ sending the request over the network, parsing the response if
+ its a menu, storing the response in a temporary file, choosing
+ and calling a handler program, and updating the history."""
+ # Don't try to speak to servers running other protocols
+ if gi.scheme in ("http", "https"):
+ webbrowser.open_new_tab(gi.url)
+ return
+ elif gi.scheme == "gopher" and not self.options.get("gopher_proxy", None):
+ print("""AV-98 does not speak Gopher natively.
+However, you can use `set gopher_proxy hostname:port` to tell it about a
+Gopher-to-Gemini proxy (such as a running Agena instance), in which case
+you'll be able to transparently follow links to Gopherspace!""")
+ return
+ elif gi.scheme not in ("gemini", "gopher"):
+ print("Sorry, no support for {} links.".format(gi.scheme))
+ return
+ # Obey permanent redirects
+ if gi.url in self.permanent_redirects:
+ new_gi = GeminiItem(self.permanent_redirects[gi.url], name=gi.name)
+ self._go_to_gi(new_gi)
+ return
+
+ # Be careful with client certificates!
+ # Are we crossing a domain boundary?
+ if self.active_cert_domains and gi.host not in self.active_cert_domains:
+ if self.active_is_transient:
+ print("Permanently delete currently active transient certificate?")
+ resp = input("Y/N? ")
+ if resp.strip().lower() in ("y", "yes"):
+ print("Destroying certificate.")
+ self._deactivate_client_cert()
+ else:
+ print("Staying here.")
+
+ else:
+ print("PRIVACY ALERT: Deactivate client cert before connecting to a new domain?")
+ resp = input("Y/N? ")
+ if resp.strip().lower() in ("n", "no"):
+ print("Keeping certificate active for {}".format(gi.host))
+ else:
+ print("Deactivating certificate.")
+ self._deactivate_client_cert()
+
+ # Suggest reactivating previous certs
+ if not self.client_certs["active"] and gi.host in self.client_certs:
+ print("PRIVACY ALERT: Reactivate previously used client cert for {}?".format(gi.host))
+ resp = input("Y/N? ")
+ if resp.strip().lower() in ("y", "yes"):
+ self._activate_client_cert(*self.client_certs[gi.host])
+ else:
+ print("Remaining unidentified.")
+ self.client_certs.pop(gi.host)
+
+ # Do everything which touches the network in one block,
+ # so we only need to catch exceptions once
+ try:
+ # Is this a local file?
+ if not gi.host:
+ address, f = None, open(gi.path, "rb")
+ else:
+ address, f = self._send_request(gi)
+
+ # Spec dictates <META> should not exceed 1024 bytes,
+ # so maximum valid header length is 1027 bytes.
+ header = f.readline(1027)
+ header = header.decode("UTF-8")
+ if not header or header[-1] != '\n':
+ raise RuntimeError("Received invalid header from server!")
+ header = header.strip()
+ self._debug("Response header: %s." % header)
+
+ # Catch network errors which may happen on initial connection
+ except Exception as err:
+ # Print an error message
+ if isinstance(err, socket.gaierror):
+ self.log["dns_failures"] += 1
+ print("ERROR: DNS error!")
+ elif isinstance(err, ConnectionRefusedError):
+ self.log["refused_connections"] += 1
+ print("ERROR: Connection refused!")
+ elif isinstance(err, ConnectionResetError):
+ self.log["reset_connections"] += 1
+ print("ERROR: Connection reset!")
+ elif isinstance(err, (TimeoutError, socket.timeout)):
+ self.log["timeouts"] += 1
+ print("""ERROR: Connection timed out!
+Slow internet connection? Use 'set timeout' to be more patient.""")
+ else:
+ print("ERROR: " + str(err))
+ return
+
+ # Validate header
+ status, meta = header.split(maxsplit=1)
+ if len(meta) > 1024 or len(status) != 2 or not status.isnumeric():
+ print("ERROR: Received invalid header from server!")
+ f.close()
+ return
+
+ # Update redirect loop/maze escaping state
+ if not status.startswith("3"):
+ self.previous_redirectors = set()
+
+ # Handle non-SUCCESS headers, which don't have a response body
+ # Inputs
+ if status.startswith("1"):
+ print(meta)
+ if status == "11":
+ user_input = getpass.getpass("> ")
+ else:
+ user_input = input("> ")
+ self._go_to_gi(gi.query(user_input))
+ return
+ # Redirects
+ elif status.startswith("3"):
+ new_gi = GeminiItem(gi.absolutise_url(meta))
+ if new_gi.url in self.previous_redirectors:
+ print("Error: caught in redirect loop!")
+ return
+ elif len(self.previous_redirectors) == _MAX_REDIRECTS:
+ print("Error: refusing to follow more than %d consecutive redirects!" % _MAX_REDIRECTS)
+ return
+ # Never follow cross-domain redirects without asking
+ elif new_gi.host != gi.host:
+ follow = input("Follow cross-domain redirect to %s? (y/n) " % new_gi.url)
+ # Never follow cross-protocol redirects without asking
+ elif new_gi.scheme != gi.scheme:
+ follow = input("Follow cross-protocol redirect to %s? (y/n) " % new_gi.url)
+ # Don't follow *any* redirect without asking if auto-follow is off
+ elif not self.options["auto_follow_redirects"]:
+ follow = input("Follow redirect to %s? (y/n) " % new_gi.url)
+ # Otherwise, follow away
+ else:
+ follow = "yes"
+ if follow.strip().lower() not in ("y", "yes"):
+ return
+ self._debug("Following redirect to %s." % new_gi.url)
+ self._debug("This is consecutive redirect number %d." % len(self.previous_redirectors))
+ self.previous_redirectors.add(gi.url)
+ if status == "31":
+ # Permanent redirect
+ self.permanent_redirects[gi.url] = new_gi.url
+ self._go_to_gi(new_gi)
+ return
+ # Errors
+ elif status.startswith("4") or status.startswith("5"):
+ print("Error: %s" % meta)
+ return
+ # Client cert
+ elif status.startswith("6"):
+ # Don't do client cert stuff in restricted mode, as in principle
+ # it could be used to fill up the disk by creating a whole lot of
+ # certificates
+ if self.restricted:
+ print("The server is requesting a client certificate.")
+ print("These are not supported in restricted mode, sorry.")
+ return
+
+ # Transient certs are a special case
+ if status == "61":
+ print("The server is asking to start a transient client certificate session.")
+ print("What do you want to do?")
+ print("1. Start a transient session.")
+ print("2. Refuse.")
+ choice = input("> ").strip()
+ if choice.strip() == "1":
+ self._generate_transient_cert_cert()
+ self._go_to_gi(gi, update_hist, handle)
+ return
+ else:
+ return
+
+ # Present different messages for different 6x statuses, but
+ # handle them the same.
+ if status in ("64", "65"):
+ print("The server rejected your certificate because it is either expired or not yet valid.")
+ elif status == "63":
+ print("The server did not accept your certificate.")
+ print("You may need to e.g. coordinate with the admin to get your certificate fingerprint whitelisted.")
+ else:
+ print("The site {} is requesting a client certificate.".format(gi.host))
+ print("This will allow the site to recognise you across requests.")
+ print("What do you want to do?")
+ print("1. Give up.")
+ print("2. Generate new certificate and retry the request.")
+ print("3. Load previously generated certificate from file.")
+ print("4. Load certificate from file and retry the request.")
+ choice = input("> ").strip()
+ if choice == "2":
+ self._generate_persistent_client_cert()
+ self._go_to_gi(gi, update_hist, handle)
+ elif choice == "3":
+ self._choose_client_cert()
+ self._go_to_gi(gi, update_hist, handle)
+ elif choice == "4":
+ self._load_client_cert()
+ self._go_to_gi(gi, update_hist, handle)
+ else:
+ print("Giving up.")
+ return
+ # Invalid status
+ elif not status.startswith("2"):
+ print("ERROR: Server returned undefined status code %s!" % status)
+ return
+
+ # If we're here, this must be a success and there's a response body
+ assert status.startswith("2")
+
+ # Can we terminate a transient client session?
+ if status == "21":
+ # Make sure we're actually in such a session
+ if self.active_is_transient:
+ self._deactivate_client_cert()
+ print("INFO: Server terminated transient client certificate session.")
+ else:
+ # Huh, that's weird
+ self._debug("Server issues a 21 but we're not in transient session?")
+
+ mime = meta
+ if mime == "":
+ mime = "text/gemini; charset=utf-8"
+ mime, mime_options = cgi.parse_header(mime)
+ if "charset" in mime_options:
+ try:
+ codecs.lookup(mime_options["charset"])
+ except LookupError:
+ print("Header declared unknown encoding %s" % value)
+ return
+
+ # Read the response body over the network
+ body = f.read()
+
+ # Save the result in a temporary file
+ ## Delete old file
+ if self.tmp_filename and os.path.exists(self.tmp_filename):
+ os.unlink(self.tmp_filename)
+ ## Set file mode
+ if mime.startswith("text/"):
+ mode = "w"
+ encoding = mime_options.get("charset", "UTF-8")
+ try:
+ body = body.decode(encoding)
+ except UnicodeError:
+ print("Could not decode response body using %s encoding declared in header!" % encoding)
+ return
+ else:
+ mode = "wb"
+ encoding = None
+ ## Write
+ tmpf = tempfile.NamedTemporaryFile(mode, encoding=encoding, delete=False)
+ size = tmpf.write(body)
+ tmpf.close()
+ self.tmp_filename = tmpf.name
+ self._debug("Wrote %d byte response to %s." % (size, self.tmp_filename))
+
+ # Pass file to handler, unless we were asked not to
+ if handle:
+ if mime == "text/gemini":
+ self._handle_index(body, gi)
+ else:
+ cmd_str = self._get_handler_cmd(mime)
+ try:
+ subprocess.call(shlex.split(cmd_str % tmpf.name))
+ except FileNotFoundError:
+ print("Handler program %s not found!" % shlex.split(cmd_str)[0])
+ print("You can use the ! command to specify another handler program or pipeline.")
+
+ # Update state
+ self.gi = gi
+ self.mime = mime
+ self._log_visit(gi, address, size)
+ if update_hist:
+ self._update_history(gi)
+ if self.always_less:
+ self.do_less()
+
+ def _send_request(self, gi):
+ """Send a selector to a given host and port.
+ Returns the resolved address and binary file with the reply."""
+ if gi.scheme == "gemini":
+ # For Gemini requests, connect to the host and port specified in the URL
+ host, port = gi.host, gi.port
+ elif gi.scheme == "gopher":
+ # For Gopher requests, use the configured proxy
+ host, port = self.options["gopher_proxy"].rsplit(":", 1)
+ self._debug("Using gopher proxy: " + self.options["gopher_proxy"])
+
+ # Do DNS resolution
+ addresses = self._get_addresses(host, port)
+
+ # Prepare TLS context
+ protocol = ssl.PROTOCOL_TLS if sys.version_info.minor >=6 else ssl.PROTOCOL_TLSv1_2
+ context = ssl.SSLContext(protocol)
+ # Use CAs or TOFU
+ if self.options["tls_mode"] == "ca":
+ context.verify_mode = ssl.CERT_REQUIRED
+ context.check_hostname = True
+ context.load_default_certs()
+ else:
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ # Impose minimum TLS version
+ ## In 3.7 and above, this is easy...
+ if sys.version_info.minor >= 7:
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
+ ## Otherwise, it seems very hard...
+ ## The below is less strict than it ought to be, but trying to disable
+ ## TLS v1.1 here using ssl.OP_NO_TLSv1_1 produces unexpected failures
+ ## with recent versions of OpenSSL. What a mess...
+ else:
+ context.options |= ssl.OP_NO_SSLv3
+ context.options |= ssl.OP_NO_SSLv2
+ # Try to enforce sensible ciphers
+ try:
+ context.set_ciphers("AESGCM+ECDHE:AESGCM+DHE:CHACHA20+ECDHE:CHACHA20+DHE:!DSS:!SHA1:!MD5:@STRENGTH")
+ except ssl.SSLError:
+ # Rely on the server to only support sensible things, I guess...
+ pass
+ # Load client certificate if needed
+ if self.client_certs["active"]:
+ certfile, keyfile = self.client_certs["active"]
+ context.load_cert_chain(certfile, keyfile)
+
+ # Connect to remote host by any address possible
+ err = None
+ for address in addresses:
+ self._debug("Connecting to: " + str(address[4]))
+ s = socket.socket(address[0], address[1])
+ s.settimeout(self.options["timeout"])
+ s = context.wrap_socket(s, server_hostname = gi.host)
+ try:
+ s.connect(address[4])
+ break
+ except OSError as e:
+ err = e
+ else:
+ # If we couldn't connect to *any* of the addresses, just
+ # bubble up the exception from the last attempt and deny
+ # knowledge of earlier failures.
+ raise err
+
+ if sys.version_info.minor >=5:
+ self._debug("Established {} connection.".format(s.version()))
+ self._debug("Cipher is: {}.".format(s.cipher()))
+
+ # Do TOFU
+ if self.options["tls_mode"] != "ca":
+ cert = s.getpeercert(binary_form=True)
+ self._validate_cert(address[4][0], host, cert)
+
+ # Remember that we showed the current cert to this domain...
+ if self.client_certs["active"]:
+ self.active_cert_domains.append(gi.host)
+ self.client_certs[gi.host] = self.client_certs["active"]
+
+ # Send request and wrap response in a file descriptor
+ self._debug("Sending %s<CRLF>" % gi.url)
+ s.sendall((gi.url + CRLF).encode("UTF-8"))
+ return address, s.makefile(mode = "rb")
+
+ def _get_addresses(self, host, port):
+ # DNS lookup - will get IPv4 and IPv6 records if IPv6 is enabled
+ if ":" in host:
+ # This is likely a literal IPv6 address, so we can *only* ask for
+ # IPv6 addresses or getaddrinfo will complain
+ family_mask = socket.AF_INET6
+ elif socket.has_ipv6 and self.options["ipv6"]:
+ # Accept either IPv4 or IPv6 addresses
+ family_mask = 0
+ else:
+ # IPv4 only
+ family_mask = socket.AF_INET
+ addresses = socket.getaddrinfo(host, port, family=family_mask,
+ type=socket.SOCK_STREAM)
+ # Sort addresses so IPv6 ones come first
+ addresses.sort(key=lambda add: add[0] == socket.AF_INET6, reverse=True)
+
+ return addresses
+
+ def _validate_cert(self, address, host, cert):
+ """
+ Validate a TLS certificate in TOFU mode.
+
+ If the cryptography module is installed:
+ - Check the certificate Common Name or SAN matches `host`
+ - Check the certificate's not valid before date is in the past
+ - Check the certificate's not valid after date is in the future
+
+ Whether the cryptography module is installed or not, check the
+ certificate's fingerprint against the TOFU database to see if we've
+ previously encountered a different certificate for this IP address and
+ hostname.
+ """
+ now = datetime.datetime.utcnow()
+ if _HAS_CRYPTOGRAPHY:
+ # Using the cryptography module we can get detailed access
+ # to the properties of even self-signed certs, unlike in
+ # the standard ssl library...
+ c = x509.load_der_x509_certificate(cert, _BACKEND)
+
+ # Check certificate validity dates
+ if c.not_valid_before >= now:
+ raise CertificateError("Certificate not valid until: {}!".format(c.not_valid_before))
+ elif c.not_valid_after <= now:
+ raise CertificateError("Certificate expired as of: {})!".format(c.not_valid_after))
+
+ # Check certificate hostnames
+ names = []
+ common_name = c.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)
+ if common_name:
+ names.append(common_name[0].value)
+ try:
+ names.extend([alt.value for alt in c.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value])
+ except x509.ExtensionNotFound:
+ pass
+ names = set(names)
+ for name in names:
+ try:
+ ssl._dnsname_match(name, host)
+ break
+ except CertificateError:
+ continue
+ else:
+ # If we didn't break out, none of the names were valid
+ raise CertificateError("Hostname does not match certificate common name or any alternative names.")
+
+ sha = hashlib.sha256()
+ sha.update(cert)
+ fingerprint = sha.hexdigest()
+
+ # Have we been here before?
+ self.db_cur.execute("""SELECT fingerprint, first_seen, last_seen, count
+ FROM cert_cache
+ WHERE hostname=? AND address=?""", (host, address))
+ cached_certs = self.db_cur.fetchall()
+
+ # If so, check for a match
+ if cached_certs:
+ max_count = 0
+ most_frequent_cert = None
+ for cached_fingerprint, first, last, count in cached_certs:
+ if count > max_count:
+ max_count = count
+ most_frequent_cert = cached_fingerprint
+ if fingerprint == cached_fingerprint:
+ # Matched!
+ self._debug("TOFU: Accepting previously seen ({} times) certificate {}".format(count, fingerprint))
+ self.db_cur.execute("""UPDATE cert_cache
+ SET last_seen=?, count=?
+ WHERE hostname=? AND address=? AND fingerprint=?""",
+ (now, count+1, host, address, fingerprint))
+ self.db_conn.commit()
+ break
+ else:
+ if _HAS_CRYPTOGRAPHY:
+ # Load the most frequently seen certificate to see if it has
+ # expired
+ certdir = os.path.join(self.config_dir, "cert_cache")
+ with open(os.path.join(certdir, most_frequent_cert+".crt"), "rb") as fp:
+ previous_cert = fp.read()
+ previous_cert = x509.load_der_x509_certificate(previous_cert, _BACKEND)
+ previous_ttl = previous_cert.not_valid_after - now
+ print(previous_ttl)
+
+ self._debug("TOFU: Unrecognised certificate {}! Raising the alarm...".format(fingerprint))
+ print("****************************************")
+ print("[SECURITY WARNING] Unrecognised certificate!")
+ print("The certificate presented for {} ({}) has never been seen before.".format(host, address))
+ print("This MIGHT be a Man-in-the-Middle attack.")
+ print("A different certificate has previously been seen {} times.".format(max_count))
+ if _HAS_CRYPTOGRAPHY:
+ if previous_ttl < datetime.timedelta():
+ print("That certificate has expired, which reduces suspicion somewhat.")
+ else:
+ print("That certificate is still valid for: {}".format(previous_ttl))
+ print("****************************************")
+ print("Attempt to verify the new certificate fingerprint out-of-band:")
+ print(fingerprint)
+ choice = input("Accept this new certificate? Y/N ").strip().lower()
+ if choice in ("y", "yes"):
+ self.db_cur.execute("""INSERT INTO cert_cache
+ VALUES (?, ?, ?, ?, ?, ?)""",
+ (host, address, fingerprint, now, now, 1))
+ self.db_conn.commit()
+ with open(os.path.join(certdir, fingerprint+".crt"), "wb") as fp:
+ fp.write(cert)
+ else:
+ raise Exception("TOFU Failure!")
+
+ # If not, cache this cert
+ else:
+ self._debug("TOFU: Blindly trusting first ever certificate for this host!")
+ self.db_cur.execute("""INSERT INTO cert_cache
+ VALUES (?, ?, ?, ?, ?, ?)""",
+ (host, address, fingerprint, now, now, 1))
+ self.db_conn.commit()
+ certdir = os.path.join(self.config_dir, "cert_cache")
+ if not os.path.exists(certdir):
+ os.makedirs(certdir)
+ with open(os.path.join(certdir, fingerprint+".crt"), "wb") as fp:
+ fp.write(cert)
+
+ def _get_handler_cmd(self, mimetype):
+ # Now look for a handler for this mimetype
+ # Consider exact matches before wildcard matches
+ exact_matches = []
+ wildcard_matches = []
+ for handled_mime, cmd_str in _MIME_HANDLERS.items():
+ if "*" in handled_mime:
+ wildcard_matches.append((handled_mime, cmd_str))
+ else:
+ exact_matches.append((handled_mime, cmd_str))
+ for handled_mime, cmd_str in exact_matches + wildcard_matches:
+ if fnmatch.fnmatch(mimetype, handled_mime):
+ break
+ else:
+ # Use "xdg-open" as a last resort.
+ cmd_str = "xdg-open %s"
+ self._debug("Using handler: %s" % cmd_str)
+ return cmd_str
+
+ def _handle_index(self, body, menu_gi, display=True):
+ self.index = []
+ preformatted = False
+ if self.idx_filename:
+ os.unlink(self.idx_filename)
+ tmpf = tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False)
+ self.idx_filename = tmpf.name
+ for line in body.splitlines():
+ if line.startswith("```"):
+ preformatted = not preformatted
+ elif preformatted:
+ tmpf.write(line + "\n")
+ elif line.startswith("=>"):
+ try:
+ gi = GeminiItem.from_map_line(line, menu_gi)
+ self.index.append(gi)
+ tmpf.write(self._format_geminiitem(len(self.index), gi) + "\n")
+ except:
+ self._debug("Skipping possible link: %s" % line)
+ elif line.startswith("* "):
+ line = line[1:].lstrip("\t ")
+ tmpf.write(textwrap.fill(line, self.options["width"],
+ initial_indent = "• ", subsequent_indent=" ") + "\n")
+ elif line.startswith(">"):
+ line = line[1:].lstrip("\t ")
+ tmpf.write(textwrap.fill(line, self.options["width"],
+ initial_indent = "> ", subsequent_indent="> ") + "\n")
+ elif line.startswith("###"):
+ line = line[3:].lstrip("\t ")
+ tmpf.write("\x1b[4m" + line + "\x1b[0m""\n")
+ elif line.startswith("##"):
+ line = line[2:].lstrip("\t ")
+ tmpf.write("\x1b[1m" + line + "\x1b[0m""\n")
+ elif line.startswith("#"):
+ line = line[1:].lstrip("\t ")
+ tmpf.write("\x1b[1m\x1b[4m" + line + "\x1b[0m""\n")
+ else:
+ tmpf.write(textwrap.fill(line, self.options["width"]) + "\n")
+ tmpf.close()
+
+ self.lookup = self.index
+ self.page_index = 0
+ self.index_index = -1
+
+ if display:
+ cmd_str = _MIME_HANDLERS["text/plain"]
+ subprocess.call(shlex.split(cmd_str % self.idx_filename))
+
+ def _format_geminiitem(self, index, gi, url=False):
+ line = "[%d] %s" % (index, gi.name or gi.url)
+ if gi.name and url:
+ line += " (%s)" % gi.url
+ return line
+
+ def _show_lookup(self, offset=0, end=None, url=False):
+ for n, gi in enumerate(self.lookup[offset:end]):
+ print(self._format_geminiitem(n+offset+1, gi, url))
+
+ def _update_history(self, gi):
+ # Don't duplicate
+ if self.history and self.history[self.hist_index] == gi:
+ return
+ self.history = self.history[0:self.hist_index+1]
+ self.history.append(gi)
+ self.hist_index = len(self.history) - 1
+
+ def _log_visit(self, gi, address, size):
+ if not address:
+ return
+ self.log["requests"] += 1
+ self.log["bytes_recvd"] += size
+ self.visited_hosts.add(address)
+ if address[0] == socket.AF_INET:
+ self.log["ipv4_requests"] += 1
+ self.log["ipv4_bytes_recvd"] += size
+ elif address[0] == socket.AF_INET6:
+ self.log["ipv6_requests"] += 1
+ self.log["ipv6_bytes_recvd"] += size
+
+ def _get_active_tmpfile(self):
+ if self.mime == "text/gemini":
+ return self.idx_filename
+ else:
+ return self.tmp_filename
+
+ def _debug(self, debug_text):
+ if not self.options["debug"]:
+ return
+ debug_text = "\x1b[0;32m[DEBUG] " + debug_text + "\x1b[0m"
+ print(debug_text)
+
+ def _load_client_cert(self):
+ """
+ Interactively load a TLS client certificate from the filesystem in PEM
+ format.
+ """
+ print("Loading client certificate file, in PEM format (blank line to cancel)")
+ certfile = input("Certfile path: ").strip()
+ if not certfile:
+ print("Aborting.")
+ return
+ elif not os.path.exists(certfile):
+ print("Certificate file {} does not exist.".format(certfile))
+ return
+ print("Loading private key file, in PEM format (blank line to cancel)")
+ keyfile = input("Keyfile path: ").strip()
+ if not keyfile:
+ print("Aborting.")
+ return
+ elif not os.path.exists(keyfile):
+ print("Private key file {} does not exist.".format(keyfile))
+ return
+ self._activate_client_cert(certfile, keyfile)
+
+ def _generate_transient_cert_cert(self):
+ """
+ Use `openssl` command to generate a new transient client certificate
+ with 24 hours of validity.
+ """
+ certdir = os.path.join(self.config_dir, "transient_certs")
+ name = str(uuid.uuid4())
+ self._generate_client_cert(certdir, name, transient=True)
+ self.active_is_transient = True
+ self.transient_certs_created.append(name)
+
+ def _generate_persistent_client_cert(self):
+ """
+ Interactively use `openssl` command to generate a new persistent client
+ certificate with one year of validity.
+ """
+ print("What do you want to name this new certificate?")
+ print("Answering `mycert` will create `~/.av98/certs/mycert.crt` and `~/.av98/certs/mycert.key`")
+ name = input()
+ if not name.strip():
+ print("Aborting.")
+ return
+ certdir = os.path.join(self.config_dir, "client_certs")
+ self._generate_client_cert(certdir, name)
+
+ def _generate_client_cert(self, certdir, basename, transient=False):
+ """
+ Use `openssl` binary to generate a client certificate (which may be
+ transient or persistent) and save the certificate and private key to the
+ specified directory with the specified basename.
+ """
+ if not os.path.exists(certdir):
+ os.makedirs(certdir)
+ certfile = os.path.join(certdir, basename+".crt")
+ keyfile = os.path.join(certdir, basename+".key")
+ cmd = "openssl req -x509 -newkey rsa:2048 -days {} -nodes -keyout {} -out {}".format(1 if transient else 365, keyfile, certfile)
+ if transient:
+ cmd += " -subj '/CN={}'".format(basename)
+ os.system(cmd)
+ self._activate_client_cert(certfile, keyfile)
+
+ def _choose_client_cert(self):
+ """
+ Interactively select a previously generated client certificate and
+ activate it.
+ """
+ certdir = os.path.join(self.config_dir, "client_certs")
+ certs = glob.glob(os.path.join(certdir, "*.crt"))
+ certdir = {}
+ for n, cert in enumerate(certs):
+ certdir[str(n+1)] = (cert, os.path.splitext(cert)[0] + ".key")
+ print("{}. {}".format(n+1, os.path.splitext(os.path.basename(cert))[0]))
+ choice = input("> ").strip()
+ if choice in certdir:
+ certfile, keyfile = certdir[choice]
+ self._activate_client_cert(certfile, keyfile)
+ else:
+ print("What?")
+
+ def _activate_client_cert(self, certfile, keyfile):
+ self.client_certs["active"] = (certfile, keyfile)
+ self.active_cert_domains = []
+ self.prompt = self.cert_prompt
+ self._debug("Using ID {} / {}.".format(*self.client_certs["active"]))
+
+ def _deactivate_client_cert(self):
+ if self.active_is_transient:
+ for filename in self.client_certs["active"]:
+ os.remove(filename)
+ for domain in self.active_cert_domains:
+ self.client_certs.pop(domain)
+ self.client_certs["active"] = None
+ self.active_cert_domains = []
+ self.prompt = self.no_cert_prompt
+ self.active_is_transient = False
+
+ # Cmd implementation follows
+
+ def default(self, line):
+ if line.strip() == "EOF":
+ return self.onecmd("quit")
+ elif line.strip() == "..":
+ return self.do_up()
+ elif line.startswith("/"):
+ return self.do_search(line[1:])
+
+ # Expand abbreviated commands
+ first_word = line.split()[0].strip()
+ if first_word in _ABBREVS:
+ full_cmd = _ABBREVS[first_word]
+ expanded = line.replace(first_word, full_cmd, 1)
+ return self.onecmd(expanded)
+
+ # Try to parse numerical index for lookup table
+ try:
+ n = int(line.strip())
+ except ValueError:
+ print("What?")
+ return
+
+ try:
+ gi = self.lookup[n-1]
+ except IndexError:
+ print ("Index too high!")
+ return
+
+ self.index_index = n
+ self._go_to_gi(gi)
+
+ ### Settings
+ @restricted
+ def do_set(self, line):
+ """View or set various options."""
+ if not line.strip():
+ # Show all current settings
+ for option in sorted(self.options.keys()):
+ print("%s %s" % (option, self.options[option]))
+ elif len(line.split()) == 1:
+ # Show current value of one specific setting
+ option = line.strip()
+ if option in self.options:
+ print("%s %s" % (option, self.options[option]))
+ else:
+ print("Unrecognised option %s" % option)
+ else:
+ # Set value of one specific setting
+ option, value = line.split(" ", 1)
+ if option not in self.options:
+ print("Unrecognised option %s" % option)
+ return
+ # Validate / convert values
+ if option == "gopher_proxy":
+ if ":" not in value:
+ value += ":1965"
+ else:
+ host, port = value.rsplit(":",1)
+ if not port.isnumeric():
+ print("Invalid proxy port %s" % port)
+ return
+ elif option == "tls_mode":
+ if value.lower() not in ("ca", "tofu"):
+ print("TLS mode must be `ca` or `tofu`!")
+ return
+ elif value.isnumeric():
+ value = int(value)
+ elif value.lower() == "false":
+ value = False
+ elif value.lower() == "true":
+ value = True
+ else:
+ try:
+ value = float(value)
+ except ValueError:
+ pass
+ self.options[option] = value
+
+ @restricted
+ def do_cert(self, line):
+ """Manage client certificates"""
+ print("Managing client certificates")
+ if self.client_certs["active"]:
+ print("Active certificate: {}".format(self.client_certs["active"][0]))
+ print("1. Deactivate client certificate.")
+ print("2. Generate new certificate.")
+ print("3. Load previously generated certificate.")
+ print("4. Load externally created client certificate from file.")
+ print("Enter blank line to exit certificate manager.")
+ choice = input("> ").strip()
+ if choice == "1":
+ print("Deactivating client certificate.")
+ self._deactivate_client_cert()
+ elif choice == "2":
+ self._generate_persistent_client_cert()
+ elif choice == "3":
+ self._choose_client_cert()
+ elif choice == "4":
+ self._load_client_cert()
+ else:
+ print("Aborting.")
+
+ @restricted
+ def do_handler(self, line):
+ """View or set handler commands for different MIME types."""
+ if not line.strip():
+ # Show all current handlers
+ for mime in sorted(_MIME_HANDLERS.keys()):
+ print("%s %s" % (mime, _MIME_HANDLERS[mime]))
+ elif len(line.split()) == 1:
+ mime = line.strip()
+ if mime in _MIME_HANDLERS:
+ print("%s %s" % (mime, _MIME_HANDLERS[mime]))
+ else:
+ print("No handler set for MIME type %s" % mime)
+ else:
+ mime, handler = line.split(" ", 1)
+ _MIME_HANDLERS[mime] = handler
+ if "%s" not in handler:
+ print("Are you sure you don't want to pass the filename to the handler?")
+
+ def do_abbrevs(self, *args):
+ """Print all AV-98 command abbreviations."""
+ header = "Command Abbreviations:"
+ self.stdout.write("\n{}\n".format(str(header)))
+ if self.ruler:
+ self.stdout.write("{}\n".format(str(self.ruler * len(header))))
+ for k, v in _ABBREVS.items():
+ self.stdout.write("{:<7} {}\n".format(k, v))
+ self.stdout.write("\n")
+
+ ### Stuff for getting around
+ def do_go(self, line):
+ """Go to a gemini URL or marked item."""
+ line = line.strip()
+ if not line:
+ print("Go where?")
+ # First, check for possible marks
+ elif line in self.marks:
+ gi = self.marks[line]
+ self._go_to_gi(gi)
+ # or a local file
+ elif os.path.exists(os.path.expanduser(line)):
+ gi = GeminiItem(None, None, os.path.expanduser(line),
+ "1", line, False)
+ self._go_to_gi(gi)
+ # If this isn't a mark, treat it as a URL
+ else:
+ self._go_to_gi(GeminiItem(line))
+
+ @needs_gi
+ def do_reload(self, *args):
+ """Reload the current URL."""
+ self._go_to_gi(self.gi)
+
+ @needs_gi
+ def do_up(self, *args):
+ """Go up one directory in the path."""
+ self._go_to_gi(self.gi.up())
+
+ def do_back(self, *args):
+ """Go back to the previous gemini item."""
+ if not self.history or self.hist_index == 0:
+ return
+ self.hist_index -= 1
+ gi = self.history[self.hist_index]
+ self._go_to_gi(gi, update_hist=False)
+
+ def do_forward(self, *args):
+ """Go forward to the next gemini item."""
+ if not self.history or self.hist_index == len(self.history) - 1:
+ return
+ self.hist_index += 1
+ gi = self.history[self.hist_index]
+ self._go_to_gi(gi, update_hist=False)
+
+ def do_next(self, *args):
+ """Go to next item after current in index."""
+ return self.onecmd(str(self.index_index+1))
+
+ def do_previous(self, *args):
+ """Go to previous item before current in index."""
+ self.lookup = self.index
+ return self.onecmd(str(self.index_index-1))
+
+ @needs_gi
+ def do_root(self, *args):
+ """Go to root selector of the server hosting current item."""
+ self._go_to_gi(self.gi.root())
+
+ def do_tour(self, line):
+ """Add index items as waypoints on a tour, which is basically a FIFO
+queue of gemini items.
+
+Items can be added with `tour 1 2 3 4` or ranges like `tour 1-4`.
+All items in current menu can be added with `tour *`.
+Current tour can be listed with `tour ls` and scrubbed with `tour clear`."""
+ line = line.strip()
+ if not line:
+ # Fly to next waypoint on tour
+ if not self.waypoints:
+ print("End of tour.")
+ else:
+ gi = self.waypoints.pop(0)
+ self._go_to_gi(gi)
+ elif line == "ls":
+ old_lookup = self.lookup
+ self.lookup = self.waypoints
+ self._show_lookup()
+ self.lookup = old_lookup
+ elif line == "clear":
+ self.waypoints = []
+ elif line == "*":
+ self.waypoints.extend(self.lookup)
+ elif looks_like_url(line):
+ self.waypoints.append(GeminiItem(line))
+ else:
+ for index in line.split():
+ try:
+ pair = index.split('-')
+ if len(pair) == 1:
+ # Just a single index
+ n = int(index)
+ gi = self.lookup[n-1]
+ self.waypoints.append(gi)
+ elif len(pair) == 2:
+ # Two endpoints for a range of indices
+ for n in range(int(pair[0]), int(pair[1]) + 1):
+ gi = self.lookup[n-1]
+ self.waypoints.append(gi)
+ else:
+ # Syntax error
+ print("Invalid use of range syntax %s, skipping" % index)
+ except ValueError:
+ print("Non-numeric index %s, skipping." % index)
+ except IndexError:
+ print("Invalid index %d, skipping." % n)
+
+ @needs_gi
+ def do_mark(self, line):
+ """Mark the current item with a single letter. This letter can then
+be passed to the 'go' command to return to the current item later.
+Think of it like marks in vi: 'mark a'='ma' and 'go a'=''a'."""
+ line = line.strip()
+ if not line:
+ for mark, gi in self.marks.items():
+ print("[%s] %s (%s)" % (mark, gi.name, gi.url))
+ elif line.isalpha() and len(line) == 1:
+ self.marks[line] = self.gi
+ else:
+ print("Invalid mark, must be one letter")
+
+ def do_version(self, line):
+ """Display version information."""
+ print("AV-98 " + _VERSION)
+
+ ### Stuff that modifies the lookup table
+ def do_ls(self, line):
+ """List contents of current index.
+Use 'ls -l' to see URLs."""
+ self.lookup = self.index
+ self._show_lookup(url = "-l" in line)
+ self.page_index = 0
+
+ def do_gus(self, line):
+ """Submit a search query to the GUS search engine."""
+ gus = GeminiItem("gemini://gus.guru/search")
+ self._go_to_gi(gus.query(line))
+
+ def do_history(self, *args):
+ """Display history."""
+ self.lookup = self.history
+ self._show_lookup(url=True)
+ self.page_index = 0
+
+ def do_search(self, searchterm):
+ """Search index (case insensitive)."""
+ results = [
+ gi for gi in self.lookup if searchterm.lower() in gi.name.lower()]
+ if results:
+ self.lookup = results
+ self._show_lookup()
+ self.page_index = 0
+ else:
+ print("No results found.")
+
+ def emptyline(self):
+ """Page through index ten lines at a time."""
+ i = self.page_index
+ if i > len(self.lookup):
+ return
+ self._show_lookup(offset=i, end=i+10)
+ self.page_index += 10
+
+ ### Stuff that does something to most recently viewed item
+ @needs_gi
+ def do_cat(self, *args):
+ """Run most recently visited item through "cat" command."""
+ subprocess.call(shlex.split("cat %s" % self._get_active_tmpfile()))
+
+ @needs_gi
+ def do_less(self, *args):
+ """Run most recently visited item through "less" command."""
+ cmd_str = self._get_handler_cmd(self.mime)
+ cmd_str = cmd_str % self._get_active_tmpfile()
+ subprocess.call("%s | less -R" % cmd_str, shell=True)
+
+ @needs_gi
+ def do_fold(self, *args):
+ """Run most recently visited item through "fold" command."""
+ cmd_str = self._get_handler_cmd(self.mime)
+ cmd_str = cmd_str % self._get_active_tmpfile()
+ subprocess.call("%s | fold -w 70 -s" % cmd_str, shell=True)
+
+ @restricted
+ @needs_gi
+ def do_shell(self, line):
+ """'cat' most recently visited item through a shell pipeline."""
+ subprocess.call(("cat %s |" % self._get_active_tmpfile()) + line, shell=True)
+
+ @restricted
+ @needs_gi
+ def do_save(self, line):
+ """Save an item to the filesystem.
+'save n filename' saves menu item n to the specified filename.
+'save filename' saves the last viewed item to the specified filename.
+'save n' saves menu item n to an automagic filename."""
+ args = line.strip().split()
+
+ # First things first, figure out what our arguments are
+ if len(args) == 0:
+ # No arguments given at all
+ # Save current item, if there is one, to a file whose name is
+ # inferred from the gemini path
+ if not self.tmp_filename:
+ print("You need to visit an item first!")
+ return
+ else:
+ index = None
+ filename = None
+ elif len(args) == 1:
+ # One argument given
+ # If it's numeric, treat it as an index, and infer the filename
+ try:
+ index = int(args[0])
+ filename = None
+ # If it's not numeric, treat it as a filename and
+ # save the current item
+ except ValueError:
+ index = None
+ filename = os.path.expanduser(args[0])
+ elif len(args) == 2:
+ # Two arguments given
+ # Treat first as an index and second as filename
+ index, filename = args
+ try:
+ index = int(index)
+ except ValueError:
+ print("First argument is not a valid item index!")
+ return
+ filename = os.path.expanduser(filename)
+ else:
+ print("You must provide an index, a filename, or both.")
+ return
+
+ # Next, fetch the item to save, if it's not the current one.
+ if index:
+ last_gi = self.gi
+ try:
+ gi = self.lookup[index-1]
+ self._go_to_gi(gi, update_hist = False, handle = False)
+ except IndexError:
+ print ("Index too high!")
+ self.gi = last_gi
+ return
+ else:
+ gi = self.gi
+
+ # Derive filename from current GI's path, if one hasn't been set
+ if not filename:
+ filename = os.path.basename(gi.path)
+
+ # Check for filename collisions and actually do the save if safe
+ if os.path.exists(filename):
+ print("File %s already exists!" % filename)
+ else:
+ # Don't use _get_active_tmpfile() here, because we want to save the
+ # "source code" of menus, not the rendered view - this way AV-98
+ # can navigate to it later.
+ shutil.copyfile(self.tmp_filename, filename)
+ print("Saved to %s" % filename)
+
+ # Restore gi if necessary
+ if index != None:
+ self._go_to_gi(last_gi, handle=False)
+
+ @needs_gi
+ def do_url(self, *args):
+ """Print URL of most recently visited item."""
+ print(self.gi.url)
+
+ ### Bookmarking stuff
+ @restricted
+ @needs_gi
+ def do_add(self, line):
+ """Add the current URL to the bookmarks menu.
+Optionally, specify the new name for the bookmark."""
+ with open(os.path.join(self.config_dir, "bookmarks.gmi"), "a") as fp:
+ fp.write(self.gi.to_map_line(line))
+
+ def do_bookmarks(self, line):
+ """Show or access the bookmarks menu.
+'bookmarks' shows all bookmarks.
+'bookmarks n' navigates immediately to item n in the bookmark menu.
+Bookmarks are stored using the 'add' command."""
+ bm_file = os.path.join(self.config_dir, "bookmarks.gmi")
+ if not os.path.exists(bm_file):
+ print("You need to 'add' some bookmarks, first!")
+ return
+ args = line.strip()
+ if len(args.split()) > 1 or (args and not args.isnumeric()):
+ print("bookmarks command takes a single integer argument!")
+ return
+ with open(bm_file, "r") as fp:
+ body = fp.read()
+ gi = GeminiItem("localhost/" + bm_file)
+ self._handle_index(body, gi, display = not args)
+ if args:
+ # Use argument as a numeric index
+ self.default(line)
+
+ ### Help
+ def do_help(self, arg):
+ """ALARM! Recursion detected! ALARM! Prepare to eject!"""
+ if arg == "!":
+ print("! is an alias for 'shell'")
+ elif arg == "?":
+ print("? is an alias for 'help'")
+ else:
+ cmd.Cmd.do_help(self, arg)
+
+ ### Flight recorder
+ def do_blackbox(self, *args):
+ """Display contents of flight recorder, showing statistics for the
+current gemini browsing session."""
+ lines = []
+ # Compute flight time
+ now = time.time()
+ delta = now - self.log["start_time"]
+ hours, remainder = divmod(delta, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ # Count hosts
+ ipv4_hosts = len([host for host in self.visited_hosts if host[0] == socket.AF_INET])
+ ipv6_hosts = len([host for host in self.visited_hosts if host[0] == socket.AF_INET6])
+ # Assemble lines
+ lines.append(("Patrol duration", "%02d:%02d:%02d" % (hours, minutes, seconds)))
+ lines.append(("Requests sent:", self.log["requests"]))
+ lines.append((" IPv4 requests:", self.log["ipv4_requests"]))
+ lines.append((" IPv6 requests:", self.log["ipv6_requests"]))
+ lines.append(("Bytes received:", self.log["bytes_recvd"]))
+ lines.append((" IPv4 bytes:", self.log["ipv4_bytes_recvd"]))
+ lines.append((" IPv6 bytes:", self.log["ipv6_bytes_recvd"]))
+ lines.append(("Unique hosts visited:", len(self.visited_hosts)))
+ lines.append((" IPv4 hosts:", ipv4_hosts))
+ lines.append((" IPv6 hosts:", ipv6_hosts))
+ lines.append(("DNS failures:", self.log["dns_failures"]))
+ lines.append(("Timeouts:", self.log["timeouts"]))
+ lines.append(("Refused connections:", self.log["refused_connections"]))
+ lines.append(("Reset connections:", self.log["reset_connections"]))
+ # Print
+ for key, value in lines:
+ print(key.ljust(24) + str(value).rjust(8))
+
+ ### The end!
+ def do_quit(self, *args):
+ """Exit AV-98."""
+ # Close TOFU DB
+ self.db_conn.commit()
+ self.db_conn.close()
+ # Clean up after ourself
+ if self.tmp_filename and os.path.exists(self.tmp_filename):
+ os.unlink(self.tmp_filename)
+ if self.idx_filename and os.path.exists(self.idx_filename):
+ os.unlink(self.idx_filename)
+ for cert in self.transient_certs_created:
+ for ext in (".crt", ".key"):
+ certfile = os.path.join(self.config_dir, "transient_certs", cert+ext)
+ if os.path.exists(certfile):
+ os.remove(certfile)
+ print()
+ print("Thank you for flying AV-98!")
+ sys.exit()
+
+ do_exit = do_quit
+
+# Main function
+def main():
+
+ # Parse args
+ parser = argparse.ArgumentParser(description='A command line gemini client.')
+ parser.add_argument('--bookmarks', action='store_true',
+ help='start with your list of bookmarks')
+ parser.add_argument('--tls-cert', metavar='FILE', help='TLS client certificate file')
+ parser.add_argument('--tls-key', metavar='FILE', help='TLS client certificate private key file')
+ parser.add_argument('--restricted', action="store_true", help='Disallow shell, add, and save commands')
+ parser.add_argument('--always-less', action="store_true", help='Always open less after navigation')
+ parser.add_argument('--version', action='store_true',
+ help='display version information and quit')
+ parser.add_argument('url', metavar='URL', nargs='*',
+ help='start with this URL')
+ args = parser.parse_args()
+
+ # Handle --version
+ if args.version:
+ print("AV-98 " + _VERSION)
+ sys.exit()
+
+ # Instantiate client
+ gc = GeminiClient(args.restricted)
+
+ # Process config file
+ rcfile = os.path.join(gc.config_dir, "av98rc")
+ if os.path.exists(rcfile):
+ print("Using config %s" % rcfile)
+ with open(rcfile, "r") as fp:
+ for line in fp:
+ line = line.strip()
+ if ((args.bookmarks or args.url) and
+ any((line.startswith(x) for x in ("go", "g", "tour", "t")))
+ ):
+ if args.bookmarks:
+ print("Skipping rc command \"%s\" due to --bookmarks option." % line)
+ else:
+ print("Skipping rc command \"%s\" due to provided URLs." % line)
+ continue
+ gc.cmdqueue.append(line)
+
+ # Say hi
+ print("Welcome to AV-98!")
+ if args.restricted:
+ print("Restricted mode engaged!")
+ print("Enjoy your patrol through Geminispace...")
+
+ gc.always_less = args.always_less
+
+ # Act on args
+ if args.tls_cert:
+ # If tls_key is None, python will attempt to load the key from tls_cert.
+ gc._activate_client_cert(args.tls_cert, args.tls_key)
+ if args.bookmarks:
+ gc.cmdqueue.append("bookmarks")
+ elif args.url:
+ if len(args.url) == 1:
+ gc.cmdqueue.append("go %s" % args.url[0])
+ else:
+ for url in args.url:
+ if not url.startswith("gemini://"):
+ url = "gemini://" + url
+ gc.cmdqueue.append("tour %s" % url)
+ gc.cmdqueue.append("tour")
+
+ # Endless interpret loop
+ while True:
+ try:
+ gc.cmdloop()
+ except KeyboardInterrupt:
+ print("")
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/awkp b/bin/awkp
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+n=${1:-1}
+
+exec awk "{print \$$n}"
diff --git a/bin/awkt b/bin/awkt
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec awk -v FS=$'\t' -v OFS=$'\t' "$@"
diff --git a/bin/baby-mic b/bin/baby-mic
@@ -0,0 +1,6 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p mplayer
+ssh monad-remote "ffmpeg -loglevel panic -f alsa -i default -f ogg -" \
+ | mplayer - -idle -demuxer ogg
+
+
diff --git a/bin/bats b/bin/bats
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+REMOVES=$(parallel bats-job ::: "$@" | sort)
+
+<<<"$REMOVES" xargs "$PAGER"
+<<<"$REMOVES" xargs rm -f
diff --git a/bin/bats-job b/bin/bats-job
@@ -0,0 +1,10 @@
+#!/usr/bin/env sh
+
+file="$1"
+base="$(basename "$file")"
+dir="$(dirname "$file")"
+colorfile="$dir/.${base}.color"
+
+printf "$colorfile\n"
+exec bat --style=full --paging=never --color=always "$file" > "$colorfile"
+#<"$file" sed '0,/^$/d;0,/^$/d' | delta > "$colorfile"
diff --git a/bin/bc-exp b/bin/bc-exp
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec sed -E 's/([+-]?[0-9.]+)[eE]\+?(-?)([0-9]+)/(\1*10^\2\3)/g'
diff --git a/bin/bcli b/bin/bcli
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+ssh 172.24.242.111 /home/jb55/bin/bcli "$@"
diff --git a/bin/bip b/bin/bip
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec curl -s gopher://jb55.com/0/bips/bip-$(printf "%04d" $1).txt | less
diff --git a/bin/bittorrent b/bin/bittorrent
@@ -0,0 +1,3 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p libcgroup transmission_gtk
+cgexec --sticky -g net_cls:pia transmission-gtk "$@"
diff --git a/bin/bright b/bin/bright
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec xbacklight -set 100
diff --git a/bin/browser b/bin/browser
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+#exec chrome "$@"
+
+kill_browser () {
+ exec pkill qutebrowser
+ #exec pkill --oldest chromium
+}
+
+if [ "$1" == "kill" ]; then
+ kill_browser
+fi
+
+#exec chrome "$@"
+qutebrowser --enable-webengine-inspector "$@"
+exec wmctrl -a qutebrowser
diff --git a/bin/btc b/bin/btc
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+cmd=${1:-price}
+shift
+
+exec btc-$cmd "$@"
diff --git a/bin/btc-balance b/bin/btc-balance
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+btc coins | awk '{print $3}' | xargs printf '%.8f * 100000000\n' | bc -l | xargs printf '%.0f\n' | paste -sd+ | bc -l | xargs printf '%s sats to btc\n' | bcalc -n
+
+if [ -n "$1" ]; then
+ FIAT=$(printf '%s sats to fiat\n' "$SATS" | bcalc --price "$1")
+ printf 'fiat\t%s\n' "$FIAT"
+fi
diff --git a/bin/btc-blockfees b/bin/btc-blockfees
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+
+# required:
+
+set -e
+
+BITCOIN_RPCUSER=${BITCOIN_RPCUSER:-rpcuser}
+BITCOIN_RPCPASS=${BITCOIN_RPCPASS:-rpcpass}
+BITCOIN_RPCPORT=${BITCOIN_RPCPORT:-8332}
+
+calc_block_subsidy() {
+ local halvings=$(($1 / 210000))
+ if [ $halvings -gt 63 ]; then
+ printf "0\n"
+ fi
+
+ local subsidy=$((50 * 100000000))
+ printf "%s\n" $((subsidy >> halvings));
+}
+
+mkreq () {
+ (
+ printf '['
+ echo ${1#","}
+ printf ']'
+ ) > /tmp/req.txt
+}
+
+doreq() {
+ mkreq "$1"
+ curl -s -u $BITCOIN_RPCUSER:$BITCOIN_RPCPASS \
+ --data-binary @/tmp/req.txt -H 'content-type: text/plain;' "http://127.0.0.1:${BITCOIN_RPCPORT}"
+}
+
+nblocks=${1:-100}
+count=$(btc blocks)
+
+heights=$(seq $((count - $nblocks + 1)) $count)
+# heights=$(seq 10 20)
+
+blockhash_reqs=$(
+ <<<"$heights" xargs printf ',{"jsonrpc": "1.0", "id":"blockfees", "method": "getblockhash", "params": [%d] }\n'
+)
+
+txid_reqs=$(
+ doreq "$blockhash_reqs" \
+ | jq -rc '.[].result' \
+ | xargs printf ',{"jsonrpc": "1.0", "id":"blockfees", "method": "getblock", "params": ["%s"] }\n'
+)
+
+tx_reqs=$(
+ doreq "$txid_reqs" \
+ | jq -rc '.[].result.tx[0]' \
+ | xargs printf ',{"jsonrpc": "1.0", "id":"blockfees", "method": "getrawtransaction", "params": ["%s", 1] }\n'
+)
+
+vals=$(doreq "$tx_reqs" | jq -rc '.[].result.vout[0].value')
+
+paste -d, <(cat <<<"$heights") <(cat <<<"$vals") | \
+while IFS=, read -r height val
+do
+ subsidy=$(calc_block_subsidy $height)
+ printf '%s-(%s/100000000)\n' "$val" "$subsidy"
+done | bc -l
diff --git a/bin/btc-blocks b/bin/btc-blocks
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec bcli "$@" getblockcount
diff --git a/bin/btc-blocktimes b/bin/btc-blocktimes
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+set -e
+
+BITCOIN_RPCUSER=rpcuser
+BITCOIN_RPCPASS=rpcpass
+BITCOIN_RPCPORT=${BITCOIN_RPCPORT:-8332}
+BITCOIN_HOST=${BITCOIN_HOST:-127.0.0.1}
+
+net=${BTCNET:-mainnet}
+
+CLI="bcli -$BTCNET"
+
+blocks=$($CLI getblockcount)
+
+n=${1:-26}
+n1=$((blocks - n + 1))
+
+mkreq () {
+ (
+ printf '['
+ echo ${1#","}
+ printf ']'
+ ) > /tmp/blocktimes-req.txt
+}
+
+doreq() {
+ local host="http://${BITCOIN_HOST}:${BITCOIN_RPCPORT}"
+ mkreq "$1"
+ curl -s -u $BITCOIN_RPCUSER:$BITCOIN_RPCPASS \
+ --data-binary @/tmp/blocktimes-req.txt \
+ -H 'content-type: text/plain;' "$host"
+}
+
+heights=$(seq $n1 $blocks)
+
+blockhash_reqs=$(
+ <<<"$heights" xargs printf ',{"jsonrpc": "1.0", "id":"blocktimes", "method": "getblockhash", "params": [%d] }\n'
+)
+
+txid_reqs=$(
+ doreq "$blockhash_reqs" \
+ | jq -rc '.[].result' \
+ | xargs printf ',{"jsonrpc": "1.0", "id":"blocktimes", "method": "getblock", "params": ["%s", 1] }\n'
+)
+
+doreq "$txid_reqs" \
+ | jq -rc '.[].result.time' \
+ | sed '$!N;s/\n/,/' \
+ | tee /tmp/blocktimes \
+ | sed -e 's/\(.*\),\(.*\)/datediff -f %S @\1 @\2/g' \
+ | sh \
+ | paste -d, <(<<<"$heights" sed 'N;s/\n/,/') /dev/stdin \
+ | cut -d, -f3 | awk '{ total += $1 } END { print (total/NR) }'
+
diff --git a/bin/btc-blocktimes-pretty b/bin/btc-blocktimes-pretty
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+btc-blocktimes "$@" | xargs printf '%s seconds\n' | xargs qalc -t
diff --git a/bin/btc-coins b/bin/btc-coins
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+WALLETS=${WALLETS:-$(bcli listwallets | jq -r '.[]' | paste -sd" " )}
+
+(for wallet in $WALLETS
+do
+ bcli -rpcwallet="$wallet" "$@" listaddressgroupings \
+ | jq -r -c ".[][] | select(.[1] != 0) | [\"$wallet\"] + . | @tsv"
+done) \
+ | column -t -s $'\t'
diff --git a/bin/btc-halvening b/bin/btc-halvening
@@ -0,0 +1,11 @@
+#!/usr/bin/env sh
+past=${1:-50}
+blocks=$(bcli getblockcount)
+blockstogo=$((210000 - ($blocks % 210000)))
+blocktimes=$(btc-blocktimes $past)
+countdown=$(qalc -t "$blocktimes seconds * $blockstogo")
+blocktimes=$(qalc -t "$blocktimes seconds")
+estimate=$(qalc -t "now + $countdown" | tr -d '"')
+
+printf "blocks\t%d\nblocks to go\t%d\naverage block time (past $past blocks)\t%s\ntime until halvening\t%s\nestimate\t%s\n" \
+ "$blocks" "$blockstogo" "$blocktimes" "$countdown" "$estimate" | ct
diff --git a/bin/btc-lastblock b/bin/btc-lastblock
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+date -d @$(bcli getblock $(bcli getblockhash $(bcli getblockcount)) | jq .time)
diff --git a/bin/btc-next-difficulty b/bin/btc-next-difficulty
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+fmt="--rfc-3339=seconds"
+past=${1:-50}
+times=$(btc-blocktimes $past)
+blocks=$(bcli getblockcount)
+prev=$(bc <<<"$blocks % 2016")
+prevblock=$((blocks - prev))
+prevdate=$(bcli getblockhash $prevblock | xargs bcli getblock | jq .time | xargs -I{} date $fmt -d@{})
+next=$((2016 - prev))
+timesnext=$(bc <<<"$times * $next")
+timesnext=${timesnext%.*}
+nextdate="$(date -d "now + $timesnext seconds" $fmt)"
+now="$(date --rfc-3339=seconds)"
+nexttime=$(datediff -f '%dd %Hh' "$now" "$nextdate")
+prevtime=$(datediff -f '%dd %Hh' "$now" "$prevdate")
+times=$(qalc -t "$times seconds")
+printf "prev\t%s\t%s\t%s\nnext\t%s\t %s\t%s\nblocktimes($past)\t%s\n" \
+ "$prev" \
+ "$prevtime" \
+ "$prevdate" \
+ "$next" \
+ "$nexttime" \
+ "$nextdate" \
+ "$times" | column -t -s $'\t'
diff --git a/bin/btc-price b/bin/btc-price
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+pair=XXBTZCAD
+cad=$(curl -sL "https://api.kraken.com/0/public/Ticker?pair=$pair" | jq -r ".result.$pair.a[0]" &)
+usd=$(curl -sL 'https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD' | jq '.[0][1]' &)
+#cad=$(curl -sL 'https://apiv2.bitcoinaverage.com/indices/global/ticker/all?crypto=BTC&fiat=CAD' | jq -r '.BTCCAD.last')
+#jcalc=$(bc -l <<<"$cad * $nbtc")
+wait
+printf "$%s USD, %s CAD\n" "$usd" "$cad"
diff --git a/bin/btc-returns b/bin/btc-returns
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+spend=$1
+low=$2
+high=$3
+
+if [ -z "$spend" ] || [ -z "$low" ] || [ -z "$high" ]
+then
+ printf 'usage: btc-returns <spend> <low> <high>\n'
+ exit 1
+fi
+
+cad=$(curl -sL 'https://api.quadrigacx.com/v2/ticker' | jq -r .last &)
+printf 'current price %s CAD\n' "$cad"
+
+# buy
+btc=$(bcalc --price "$low" "$spend" fiat to btc)
+printf 'spend %s @ %s CAD of btc -> %s\n' "$spend" "$low" "$btc"
+
+fiat_profit=$(echo "$(bcalc -n --price "$high" "$btc" to fiat) - $low" | bc -l)
+
+btc_profit=$(bcalc --price "$high" "$fiat_profit" fiat to btc)
+
+printf '\nfiat profit %s, btc profit %s @ %s CAD/BTC\n' "$fiat_profit" \
+ "$btc_profit" "$high"
+
diff --git a/bin/btc-tx-cost b/bin/btc-tx-cost
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf "usage: %s <inputs> <outputs> <sat/b>" "$0"
+ exit 1
+}
+
+[ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] && usage
+
+inputs="$1"
+outputs="$2"
+satsb="$3"
+
+
+btc=$(printf "(%d * 180 + %d * 34 + 10) * %d * 0.00000001 \n" \
+ "$inputs" "$outputs" "$satsb" | bc -l)
+
+mbtc=$(printf "%f * 1000\n" "$btc" | bc -l)
+
+price=$(btc)
+cad=$(printf "%f * %f\n" "$btc" "$price" | bc -l)
+
+printf "%f BTC\n%f mBTC\n%f CAD (@ ${price}/btc)\n" "$btc" "$mbtc" "$cad"
diff --git a/bin/btc-txs b/bin/btc-txs
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec btc-txs-raw | ct | $PAGER
diff --git a/bin/btc-whoa b/bin/btc-whoa
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+while true
+do
+ if bcli waitfornewblock; then
+ block=$(bcli getblockcount)
+ notify-send -u critical "$(btc-halvening)"
+ if [ "$block" -eq 630000 ]; then
+ notify-send -u critical 'Happy Halvening!'
+ play /home/jb55/var/audio/whoa.ogg
+ fi
+ fi
+done
diff --git a/bin/capcom b/bin/capcom
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec gemini gemini://gemini.circumlunar.space/capcom
diff --git a/bin/cert b/bin/cert
@@ -0,0 +1,14 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p openssl
+out=$(echo "Q" | openssl s_client -connect "$1":"${2:-443}" -servername "$1")
+
+cert=$(sed -ne '
+ /-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p # got the range, ok
+ /-END CERTIFICATE-/q # bailing out soon as the cert end seen
+' <<<"$out")
+
+(
+echo "$cert" | openssl x509 -fingerprint -sha1 -in - -text -noout
+
+printf "%s\n" "$out"
+) | less
diff --git a/bin/chat b/bin/chat
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+HOST=$(hostname)
+CHATFILE=/home/jb55/famchat
+
+if [ "$HOST" = "monad" ]; then
+ USER=will
+elif [ "$HOST" = "quiver" ]; then
+ USER=will
+else
+ USER=vanessa
+fi
+
+update () {
+ scp jb55@192.168.86.26:bin/chat "$HOME/bin/chat"
+}
+
+if [ "$USER" = "vanessa" ]; then
+ update
+fi
+
+out () {
+ if [ "$HOST" = "monad" ]; then
+ cat >> "$CHATFILE" && tail -n100 "$CHATFILE"
+ elif [ "$HOST" = "quiver" ]; then
+ ssh 172.24.242.111 "cat >> $CHATFILE && tail -n100 $CHATFILE"
+ else
+ ssh jb55@192.168.86.26 "cat >> $CHATFILE && tail -n100 $CHATFILE"
+ fi
+}
+
+show () {
+ if [ "$HOST" = "monad" ]; then
+ tail -n100 "$CHATFILE"
+ elif [ "$HOST" = "quiver" ]; then
+ ssh 172.24.242.111 "tail -n100 $CHATFILE"
+ else
+ ssh jb55@192.168.86.26 "tail -n100 $CHATFILE"
+ fi
+}
+
+if [ -n "$1" ]; then
+ printf "$(date +'%F %R') | %s\n%s\n\n" "$USER" "$*" | out
+else
+ show
+fi
+
diff --git a/bin/chrome b/bin/chrome
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+OPTS=""
+HOST="$(hostname)"
+
+if [ "$HOST" == "quiver" ]; then
+ OPTS="--force-device-scale-factor=1.25"
+fi
+
+exec chromium "$OPTS" "$@"
diff --git a/bin/chromecast b/bin/chromecast
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+castnow --command "$1" --exit && printf "chromecast $2\n"+
\ No newline at end of file
diff --git a/bin/chromecast-stop b/bin/chromecast-stop
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+castnow --command s --exit && printf "chromecast stopped\n"
+
diff --git a/bin/clearmime b/bin/clearmime
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+# Copyright 2008 Lenny Domnitser <http://domnit.org/>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+__all__ = 'clarify',
+__author__ = 'Lenny Domnitser'
+__version__ = '0.1'
+
+import email
+import re
+
+TEMPLATE = '''-----BEGIN PGP SIGNED MESSAGE-----
+Hash: %(hashname)s
+NotDashEscaped: You need GnuPG to verify this message
+
+%(text)s%(sig)s'''
+
+
+def _clarify(message, messagetext):
+ if message.get_content_type() == 'multipart/signed':
+ if message.get_param('protocol') == 'application/pgp-signature':
+ hashname = message.get_param('micalg').upper()
+ assert hashname.startswith('PGP-')
+ hashname = hashname.replace('PGP-', '', 1)
+ textmess, sigmess = message.get_payload()
+ assert sigmess.get_content_type() == 'application/pgp-signature'
+ #text = textmess.as_string() - not byte-for-byte accurate
+ text = messagetext.split('\n--%s\n' % message.get_boundary(), 2)[1]
+ sig = sigmess.get_payload()
+ assert isinstance(sig, str)
+ # Setting content-type to application/octet instead of text/plain
+ # to maintain CRLF endings. Using replace_header instead of
+ # set_type because replace_header clears parameters.
+ message.replace_header('Content-Type', 'application/octet')
+ clearsign = TEMPLATE % locals()
+ clearsign = clearsign.replace(
+ '\r\n', '\n').replace('\r', '\n').replace('\n', '\r\n')
+ message.set_payload(clearsign)
+ elif message.is_multipart():
+ for message in message.get_payload():
+ _clarify(message, messagetext)
+
+
+def clarify(messagetext):
+ '''given a string containing a MIME message, returns a string
+ where PGP/MIME messages are replaced with clearsigned messages.'''
+
+ message = email.message_from_string(messagetext)
+ _clarify(message, messagetext)
+ return message.as_string()
+
+
+if __name__ == '__main__':
+ import sys
+ sys.stdout.write(clarify(sys.stdin.read()))
diff --git a/bin/cmd b/bin/cmd
@@ -0,0 +1,3 @@
+#!/bin/bash
+CUR="`pwd`"
+echo "cd $PWD && $@" > /tmp/cmds
diff --git a/bin/colorpick b/bin/colorpick
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+picked=$(colorpicker --one-shot --short)
+
+<<<"$picked" tr -d "\n$" | xclip
+notify-send "$picked"
diff --git a/bin/columnt b/bin/columnt
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec column -t -s $'\t' "$@"
diff --git a/bin/commands b/bin/commands
@@ -0,0 +1,4 @@
+#!/bin/sh
+FIFO=${1:-"/tmp/cmds"}
+mkfifo $FIFO &> /dev/null
+while :; do bash < $FIFO && echo "== OK ==" || echo "!! ERR !!"; done
diff --git a/bin/connect-bose b/bin/connect-bose
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+bluetoothctl <<<"connect 00:0C:8A:70:43:60"+
\ No newline at end of file
diff --git a/bin/coretto-emails b/bin/coretto-emails
@@ -0,0 +1,6 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p sqlite dateutils
+
+set -e
+
+ssh charon sqlite3 -csv www/coretto.io/emails.db "'select * from emails'"
diff --git a/bin/cs b/bin/cs
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec curl cheat.sh/$1
diff --git a/bin/ct b/bin/ct
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec column -t -s $'\t' "$@"
diff --git a/bin/curlsl b/bin/curlsl
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec curl -sL "$@"
diff --git a/bin/cutt b/bin/cutt
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec cut -d $'\t' --output-delimiter=$'\t' "$@"
diff --git a/bin/dclip b/bin/dclip
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec clipmenu "$@" -i -fn Inconsolata:size=14 -p clip -l 40 -w 700
diff --git a/bin/devpeople b/bin/devpeople
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+peeps=$(cat <<EOP
+Australia/Adelaide,rusty
+Europe/Malta,adamback
+Europe/Zurich,cdecker
+Europe/Rome,lawrence
+Europe/Rome,alekos,10:00,18:00
+EOP
+)
+
+(printf "name time start end pt_start pt_end\n";
+while IFS=, read -r tz person start end
+do
+ d=$(TZ="$tz" date +'%a %F %R')
+ start_local=""
+ end_local=""
+ if [[ -n $start ]]; then
+ start_local=$(date --date 'TZ="'$tz'" '$start +%R)
+ end_local=$(date --date 'TZ="'$tz'" '$end +%R)
+ fi
+ printf "$person $d $start $end $start_local $end_local\n"
+done <<<"$peeps") | column -t -s $'\t'
diff --git a/bin/dmenu b/bin/dmenu
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec /run/current-system/sw/bin/dmenu -fn terminus-12 "$@"
diff --git a/bin/dmenu-lpass b/bin/dmenu-lpass
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+set -e
+
+export LPASS_HOME=${LPASS_HOME:-"$HOME"/.config/lpass}
+
+login() {
+ lpass login jackbox55@gmail.com
+}
+
+if [ ! -f "$LPASS_HOME"/session_privatekey ]
+then
+ login
+fi
+
+IFS=$'\n'
+# List all entries in LastPass vault into dmenu formatted as follows
+# Folder/subfolder/Name of Site [username at site] [id: id for lookup]
+entries=($(lpass ls --long \
+ | cut -d ' ' -f 3- \
+ | sed 's/\[username: /[/;s/\(.*\)\(\[.*\]\) \(\[.*\]\)/\1 \3 \2/')
+ )
+
+# Get id from dmenu user selection
+selid=$(printf '%s\n' "${entries[@]}" \
+ | dmenu -i -p 'LastPass' -l 7 \
+ | sed 's/^.*\[id: \([0-9]\{1,\}\)\].*$/\1/')
+
+# Password username and password to clipboard
+lpass show --clip --user "$selid"
+lpass show --clip --password "$selid"
diff --git a/bin/dmenup b/bin/dmenup
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -e
+
+if [ -z "$1" ]; then
+ printf 'usage: dmenup <prompt> <command> [cmdargs...]\n' >&2
+ exit 1
+fi
+
+prompt="$1"
+shift
+
+out=$(dmenu -noinput -p "$prompt")
+
+"$@" $out
diff --git a/bin/dmenupn b/bin/dmenupn
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+out="$(dmenup "$@")"
+
+xclip <<<"$out"
+notify-send "$out"
diff --git a/bin/dog b/bin/dog
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+[[ -z $1 ]] && exit 1
+cat $(spath $1)
diff --git a/bin/ds4-connect b/bin/ds4-connect
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+bluetoothctl << EOF
+power on
+agent on
+default-agent
+trust 84:17:66:BB:F2:4D
+connect 84:17:66:BB:F2:4D
+#trust 30:0E:D5:80:AA:89
+#connect 30:0E:D5:80:AA:89
+EOF
diff --git a/bin/ds4-disconnect b/bin/ds4-disconnect
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+bluetoothctl << EOF
+disconnect 84:17:66:BB:F2:4D
+disconnect 30:0E:D5:80:AA:89
+EOF
diff --git a/bin/ds4_battery b/bin/ds4_battery
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+for controller in /sys/class/power_supply/sony_controller_battery*; do
+ capacity=$(<"$controller/capacity" tr '\n' '%')
+ status=$(cat "$controller/status")
+ echo -n "$capacity"
+ [[ "$status" = "Charging" ]] && echo -n "+" || echo -n "-"
+done
+
diff --git a/bin/dswitcher b/bin/dswitcher
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+remove_useless() {
+ if [ -z "$1" ]
+ then
+ cat
+ else
+ grep -v "$1"
+ fi
+}
+
+# dmenu cannot display more than 30 lines, to avoid screen clutter. Only relevant if you have more than 30 windows open.
+height=$(wmctrl -l | wc -l)
+if [[ $height -gt 30 ]]
+ then heightfit=30
+ else heightfit=$height
+fi
+
+window=$(wmctrl -xl \
+ | remove_useless "$1" \
+ | sed 's/ / /' \
+ | sed -r 's/^(0x.{8}) ([0-9]+) [^ \t]+\.([^ \t]+)[ \t]+[^ \t]+[ \t]+(.*)$/\2 - \3: \4 [\1]/' \
+ | dmenu -i -p "windows" -l $heightfit \
+ | sed -rn 's,.*\[(.{10})\],\1,p')
+
+[[ -z "$window" ]] && exit
+wmctrl -i -a "$window"
diff --git a/bin/dupfiles b/bin/dupfiles
@@ -0,0 +1,2 @@
+#!/bin/sh
+find . -maxdepth 1 -type f | xargs sha1sum | sort -k1,1 | uniq -c -w40 | sort -n
diff --git a/bin/dynamic-linker b/bin/dynamic-linker
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+cat $(nix-eval -n '"${stdenv.cc}/nix-support/dynamic-linker"' | sed 's/"//g')
diff --git a/bin/edit b/bin/edit
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+emacsclient -s $HOME/.emacs.d/server/server -a vim "$@" &
+wmctrl -a emacs@$HOST
+wait
diff --git a/bin/edit-clipboard b/bin/edit-clipboard
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+xclip -o | vipe | xclip
diff --git a/bin/emacs-dev b/bin/emacs-dev
@@ -0,0 +1,2 @@
+#!/bin/sh
+all-dev --run 'emacs & disown'
diff --git a/bin/emacs-mailto-handler b/bin/emacs-mailto-handler
@@ -0,0 +1,31 @@
+#!/usr/bin/env sh
+# emacs-mailto-handler
+# From http://www.emacswiki.org/emacs/MailtoHandler, 31 July 2010.
+
+# Takes a mailto link as its argument and pass it to Emacs.
+
+# For example, using the Mozex extension for Firefox, set the mailer to:
+# emacs-mailto-handler %r
+# (you may need to specify the full pathname of emacs-mailto-handler)
+# and add to your ~/.emacs:
+# (autoload 'mailto-compose-mail "mailto-compose-mail")
+
+
+mailto=$1
+mailto="${mailto#mailto:}"
+mailto=$(printf '%s\n' "$mailto" | sed -e 's/[\"]/\\&/g')
+
+#elisp_expr="(compose-mail \"$mailto\")"
+elisp_expr="(notmuch-compose-mail \"$mailto\")"
+
+## This version re-uses an existing window.
+#emacsclient -n --eval "$elisp_expr"
+
+exec emacsclient -s $HOME/.emacs.d/server/server -n -c \
+ --eval "$elisp_expr" \
+ '(set-window-dedicated-p (selected-window) t)'
+
+## This version creates a fresh window.
+# emacsclient -a "" -c -n --eval "$elisp_expr" \
+# '(set-window-dedicated-p (selected-window) t)'
+
diff --git a/bin/emacsc b/bin/emacsc
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+emacsclient -c "$@" &> /dev/null || emacs "$@" &>/dev/null & disown
diff --git a/bin/email-conn-test b/bin/email-conn-test
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec nc -w 1 -vz jb55.com 12566
diff --git a/bin/email-fetch b/bin/email-fetch
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec sysctlu restart email-fetcher
diff --git a/bin/email-status b/bin/email-status
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+queued=$(ls -1 /home/jb55/.msmtp.queue/*.mail | wc -l)
+if [ $queued -gt 0 ]; then
+ printf "%d queued emails\n" "$queued"
+fi
+out=$(jctlu -n1 -u email-fetcher | tail -n1)
+printf "$out\n"
diff --git a/bin/email-status-once b/bin/email-status-once
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+service=email-fetcher
+
+queued=$(ls -1 /home/jb55/.msmtp.queue | grep \.mail | wc -l)
+
+time=$(journalctl --user -n1 --no-pager --output=short-iso -u $service | tail -n1 | cut -d " " -f1 | xargs -I{} date -d {} '+%F %R')
+
+diff=$(datediff --format="%Hh:%Mm" "$time" "$(date '+%F %R')")
+
+last_email=$(notmuch search --format=json --limit=1 --sort=newest-first date:today | jq -r '.[0].date_relative')
+
+printf "last email $last_email | fetched $diff ago | $queued queued | "
+journalctl --user -n1 --no-pager --output=cat -u $service
diff --git a/bin/fetch-work-mail b/bin/fetch-work-mail
@@ -0,0 +1,3 @@
+#!/bin/sh
+mbsync gmail
+notmuch-work new
diff --git a/bin/find-dir b/bin/find-dir
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+NAME="$1"
+shift
+find . -name $NAME -type d $@
diff --git a/bin/find-file b/bin/find-file
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+NAME="$1"
+shift
+find . -name "$NAME" -type f "$@"
diff --git a/bin/focus-wow b/bin/focus-wow
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec wmctrl -a 'World of Warcraft'+
\ No newline at end of file
diff --git a/bin/focus-zoom b/bin/focus-zoom
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+wmctrl -a Participant || \
+wmctrl -a Zoom
+
diff --git a/bin/fsize b/bin/fsize
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec stat -c %s "$1"
diff --git a/bin/gaps b/bin/gaps
@@ -0,0 +1,2 @@
+#!/bin/sh
+awk '$1!=p+1{print p+1"-"$1-1}{p=$1}' "$@"
diff --git a/bin/gemini b/bin/gemini
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec reader $GEMINICLIENT --always-less "$@"
diff --git a/bin/gh b/bin/gh
Binary files differ.
diff --git a/bin/gh-add-refs b/bin/gh-add-refs
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+git config \
+ --file=".git/config" \
+ --add remote.origin.fetch '+refs/pull/*/head:refs/pull/origin/*'
+
+git config \
+ --file=".git/config" \
+ --add remote.origin.fetch '+refs/pull/*/merge:refs/merge/origin/*'
diff --git a/bin/gh-clone b/bin/gh-clone
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+
+set -e
+
+usage () {
+ printf "usage: gh-clone <owner>[/]<repo>\n" >&2
+ exit 1
+}
+
+already_exists () {
+ printf "%s\n" "$2"
+ exit 2
+}
+
+root=${GITHUB_ROOT:-"$HOME/dev/github"}
+owner="$1"
+
+shift
+
+[ -z "$owner" ] && usage
+
+IFS='/' read -ra parsed <<< "$owner"
+owner="${parsed[0]}"
+repo="${parsed[1]}"
+if [ -z "$owner" ] || [ -z "$repo" ]; then
+ usage
+fi
+
+dest="$root/$owner"
+dir="$dest/$repo"
+
+[ -d "$dir" ] && already_exists "$owner/$repo" "$dir"
+
+mkdir -p "$dest"
+cd "$dest"
+git clone "$@" "gh:$owner/$repo"
+
+git config \
+ --file="$dir/.git/config" \
+ --add remote.origin.fetch '+refs/pull/*/head:refs/pull/origin/*'
+
+git config \
+ --file="$dir/.git/config" \
+ --add remote.origin.fetch '+refs/pull/*/merge:refs/merge/origin/*'
+
+printf "%s\n" "$dest/$repo"
diff --git a/bin/ghcWithPackages b/bin/ghcWithPackages
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; ["$@"])"
diff --git a/bin/ghissue b/bin/ghissue
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec open "https://github.com/$1/$2/issues/$3"
diff --git a/bin/gifenc b/bin/gifenc
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+palette="/tmp/palette.png"
+
+filters="fps=15,scale=320:-1:flags=lanczos"
+
+ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
+ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
diff --git a/bin/git-author-stats b/bin/git-author-stats
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+script=$(cat << 'EOF'
+NF == 1 { name = $1; commits[name] += 1 }
+NF == 3 { plus[name] += $1; minus[name] += $2 }
+
+END {
+ for (name in plus) {
+ print name "\t+" plus[name] "\t-" minus[name] "\t" commits[name]
+ }
+}
+
+EOF
+)
+
+REVERSE=r
+
+if [ -z $ASTATS_REVERSE ]; then
+ REVERSE=""
+fi
+
+
+(printf $'name\tadded\tremoved\tcommits\n';
+
+git log --numstat --pretty=$'%aN' "$@" \
+ | awk -F$'\t' "$script" \
+ | sort --field-separator=$'\t' -k${ASTATS_SORT_COLUMN:-2} -g${REVERSE}
+) > /tmp/git-author-stats
+
+column -t -s $'\t' /tmp/git-author-stats
+rm /tmp/git-author-stats
diff --git a/bin/git-bvr b/bin/git-bvr
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec git branch -vr --sort=-committerdate "$@" | $FUZZER --no-sort --exact | awk '{ print $1 }'
diff --git a/bin/git-checkout-pr b/bin/git-checkout-pr
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+PR="$1"
+
+exec git checkout -b pr${PR} refs/pull/origin/${PR}
diff --git a/bin/git-checkout-remote b/bin/git-checkout-remote
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -e
+
+usage() {
+ printf "usage: git-checkout-remote <remote> <branch>\n"
+ exit 1
+}
+
+REMOTE="$1"
+BRANCH="$2"
+
+if [ -z "$REMOTE" ] || [ -z "$BRANCH" ]; then
+ usage
+fi
+
+shift
+shift
+
+git fetch "$REMOTE" "$BRANCH" "$@"
+
+exec git checkout -b "$BRANCH" FETCH_HEAD
diff --git a/bin/git-cherry-pick-squashed b/bin/git-cherry-pick-squashed
diff --git a/bin/git-contacts b/bin/git-contacts
@@ -0,0 +1,203 @@
+#!/usr/bin/env perl
+
+# List people who might be interested in a patch. Useful as the argument to
+# git-send-email --cc-cmd option, and in other situations.
+#
+# Usage: git contacts <file | rev-list option> ...
+
+use strict;
+use warnings;
+use IPC::Open2;
+
+my $since = '5-years-ago';
+my $min_percent = 10;
+my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc|Reported-by/i;
+my %seen;
+
+sub format_contact {
+ my ($name, $email) = @_;
+ return "$name <$email>";
+}
+
+sub parse_commit {
+ my ($commit, $data) = @_;
+ my $contacts = $commit->{contacts};
+ my $inbody = 0;
+ for (split(/^/m, $data)) {
+ if (not $inbody) {
+ if (/^author ([^<>]+) <(\S+)> .+$/) {
+ $contacts->{format_contact($1, $2)} = 1;
+ } elsif (/^$/) {
+ $inbody = 1;
+ }
+ } elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) {
+ $contacts->{format_contact($1, $2)} = 1;
+ }
+ }
+}
+
+sub import_commits {
+ my ($commits) = @_;
+ return unless %$commits;
+ my $pid = open2 my $reader, my $writer, qw(git cat-file --batch);
+ for my $id (keys(%$commits)) {
+ print $writer "$id\n";
+ my $line = <$reader>;
+ if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) {
+ my ($cid, $len) = ($1, $2);
+ die "expected $id but got $cid\n" unless $id eq $cid;
+ my $data;
+ # cat-file emits newline after data, so read len+1
+ read $reader, $data, $len + 1;
+ parse_commit($commits->{$id}, $data);
+ }
+ }
+ close $reader;
+ close $writer;
+ waitpid($pid, 0);
+ die "git-cat-file error: $?\n" if $?;
+}
+
+sub get_blame {
+ my ($commits, $source, $from, $ranges) = @_;
+ return unless @$ranges;
+ open my $f, '-|',
+ qw(git blame --porcelain -C),
+ map({"-L$_->[0],+$_->[1]"} @$ranges),
+ '--since', $since, "$from^", '--', $source or die;
+ while (<$f>) {
+ if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) {
+ my $id = $1;
+ $commits->{$id} = { id => $id, contacts => {} }
+ unless $seen{$id};
+ $seen{$id} = 1;
+ }
+ }
+ close $f;
+}
+
+sub blame_sources {
+ my ($sources, $commits) = @_;
+ for my $s (keys %$sources) {
+ for my $id (keys %{$sources->{$s}}) {
+ get_blame($commits, $s, $id, $sources->{$s}{$id});
+ }
+ }
+}
+
+sub scan_patches {
+ my ($sources, $id, $f) = @_;
+ my $source;
+ while (<$f>) {
+ if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) {
+ $id = $1;
+ $seen{$id} = 1;
+ }
+ next unless $id;
+ if (m{^--- (?:a/(.+)|/dev/null)$}) {
+ $source = $1;
+ } elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) {
+ my $len = defined($2) ? $2 : 1;
+ push @{$sources->{$source}{$id}}, [$1, $len] if $len;
+ }
+ }
+}
+
+sub scan_patch_file {
+ my ($commits, $file) = @_;
+ open my $f, '<', $file or die "read failure: $file: $!\n";
+ scan_patches($commits, undef, $f);
+ close $f;
+}
+
+sub parse_rev_args {
+ my @args = @_;
+ open my $f, '-|',
+ qw(git rev-parse --revs-only --default HEAD --symbolic), @args
+ or die;
+ my @revs;
+ while (<$f>) {
+ chomp;
+ push @revs, $_;
+ }
+ close $f;
+ return @revs if scalar(@revs) != 1;
+ return "^$revs[0]", 'HEAD' unless $revs[0] =~ /^-/;
+ return $revs[0], 'HEAD';
+}
+
+sub scan_rev_args {
+ my ($commits, $args) = @_;
+ my @revs = parse_rev_args(@$args);
+ open my $f, '-|', qw(git rev-list --reverse), @revs or die;
+ while (<$f>) {
+ chomp;
+ my $id = $_;
+ $seen{$id} = 1;
+ open my $g, '-|', qw(git show -C --oneline), $id or die;
+ scan_patches($commits, $id, $g);
+ close $g;
+ }
+ close $f;
+}
+
+sub mailmap_contacts {
+ my ($contacts) = @_;
+ my %mapped;
+ my $pid = open2 my $reader, my $writer, qw(git check-mailmap --stdin);
+ for my $contact (keys(%$contacts)) {
+ print $writer "$contact\n";
+ my $canonical = <$reader>;
+ chomp $canonical;
+ $mapped{$canonical} += $contacts->{$contact};
+ }
+ close $reader;
+ close $writer;
+ waitpid($pid, 0);
+ die "git-check-mailmap error: $?\n" if $?;
+ return \%mapped;
+}
+
+if (!@ARGV) {
+ die "No input revisions or patch files\n";
+}
+
+my (@files, @rev_args);
+for (@ARGV) {
+ if (-e) {
+ push @files, $_;
+ } else {
+ push @rev_args, $_;
+ }
+}
+
+my %sources;
+for (@files) {
+ scan_patch_file(\%sources, $_);
+}
+if (@rev_args) {
+ scan_rev_args(\%sources, \@rev_args)
+}
+
+my $toplevel = `git rev-parse --show-toplevel`;
+chomp $toplevel;
+chdir($toplevel) or die "chdir failure: $toplevel: $!\n";
+
+my %commits;
+blame_sources(\%sources, \%commits);
+import_commits(\%commits);
+
+my $contacts = {};
+for my $commit (values %commits) {
+ for my $contact (keys %{$commit->{contacts}}) {
+ $contacts->{$contact}++;
+ }
+}
+$contacts = mailmap_contacts($contacts);
+
+my $ncommits = scalar(keys %commits);
+for my $contact (keys %$contacts) {
+ my $percent = $contacts->{$contact} * 100 / $ncommits;
+ next if $percent < $min_percent;
+ print "$contact\n";
+}
diff --git a/bin/git-find-blob b/bin/git-find-blob
@@ -0,0 +1,48 @@
+#!/usr/bin/env perl
+use 5.008;
+use strict;
+use Memoize;
+
+my $obj_name;
+
+sub check_tree {
+ my ( $tree ) = @_;
+ my @subtree;
+
+ {
+ open my $ls_tree, '-|', git => 'ls-tree' => $tree
+ or die "Couldn't open pipe to git-ls-tree: $!\n";
+
+ while ( <$ls_tree> ) {
+ /\A[0-7]{6} (\S+) (\S+)/
+ or die "unexpected git-ls-tree output";
+ return 1 if $2 eq $obj_name;
+ push @subtree, $2 if $1 eq 'tree';
+ }
+ }
+
+ check_tree( $_ ) && return 1 for @subtree;
+
+ return;
+}
+
+memoize 'check_tree';
+
+die "usage: git-find-blob <blob> [<git-log arguments ...>]\n"
+ if not @ARGV;
+
+my $obj_short = shift @ARGV;
+$obj_name = do {
+ local $ENV{'OBJ_NAME'} = $obj_short;
+ `git rev-parse --verify \$OBJ_NAME`;
+} or die "Couldn't parse $obj_short: $!\n";
+chomp $obj_name;
+
+open my $log, '-|', git => log => @ARGV, '--pretty=format:%T %h %s'
+ or die "Couldn't open pipe to git-log: $!\n";
+
+while ( <$log> ) {
+ chomp;
+ my ( $tree, $commit, $subject ) = split " ", $_, 3;
+ print "$commit $subject\n" if check_tree( $tree );
+}
diff --git a/bin/git-fixup b/bin/git-fixup
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+commit=$(git rev-parse ${1:-HEAD})
+git commit --fixup=$commit
+GIT_EDITOR=: git rebase -i --autosquash ${commit}^
diff --git a/bin/git-format-patches b/bin/git-format-patches
diff --git a/bin/git-gh-merge b/bin/git-gh-merge
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+BR=$1
+PR=$2
+MSG="$3"
+
+usage () {
+ printf "usage: git gh-merge origin/branch <github-pr#> <msg>\n"
+ exit 1
+}
+
+if [ "$#" -ne 3 ]; then
+ usage
+fi
+
+exec git merge --no-ff "$BR" -m "Merge [#$PR] $MSG"+
\ No newline at end of file
diff --git a/bin/git-github-refs b/bin/git-github-refs
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+git config --add remote.origin.fetch '+refs/pull/*/head:refs/pull/origin/*'
+git config --add remote.origin.fetch '+refs/pull/*/merge:refs/merge/origin/*'
+
+
diff --git a/bin/git-logbr b/bin/git-logbr
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+branch="$1"
+shift
+exec git log --reverse "$@" origin/master.."$branch"
diff --git a/bin/git-logm b/bin/git-logm
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+commit=${1:-HEAD}
+
+shift
+
+exec git log "$@" ${commit}^1..${commit}^2
diff --git a/bin/git-logr b/bin/git-logr
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+ancestor=${1:-HEAD}
+descendent=${2:-origin/master}
+shift
+shift
+exec git log --graph --oneline ^$ancestor^ $descendent --ancestry-path "$@"
diff --git a/bin/git-pr b/bin/git-pr
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf "usage: git prlog <pr>\n" >&2
+ exit 1
+}
+[ -z "$1" ] && usage
+
+ref="$1"
+shift
+exec git log --reverse "$@" "refs/merge/origin/$ref^..refs/pull/origin/$ref"
diff --git a/bin/git-pr-event b/bin/git-pr-event
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+
+set -e
+
+access_token=${GITHUB_ACCESS_TOKEN}
+
+api() {
+ curl -sL -H "Authorization: token $access_token" "$@"
+}
+
+usage() {
+ printf "usage: git-pr-event <owner>/<repo> <pr> <body> <approve|reject|comment>\n" >&2
+ exit 1
+}
+
+getevent() {
+ case "$1" in
+ "approve")
+ printf "APPROVE\n"
+ ;;
+ "reject")
+ printf "REQUEST_CHANGES\n"
+ ;;
+ "comment")
+ printf "COMMENT\n"
+ ;;
+ *)
+ printf "invalid event '%s', choose approve, reject or comment\n" "$1" >&2
+ exit 1
+ ;;
+ esac
+}
+
+project="$1"
+pr="$2"
+body="$3"
+event="${4:-approve}"
+
+if [ -z $project ] || [ -z $pr ] || [ -z $body ]; then
+ usage
+fi
+
+base_url='https://api.github.com/repos/'"$project"
+
+event=$(getevent $event)
+
+review_id=$(api -X POST -d'{"body":"'"$body"'"}' "$base_url/pulls/$pr/reviews" | jq -r .id)
+
+api -X POST -d '{"body":"'"$body"'", "event":"'"$event"'"}' \
+ "$base_url/pulls/$pr/reviews/$review_id/events" | jq -r '._links.html.href'
+
diff --git a/bin/git-review b/bin/git-review
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+set -e
+
+REMOTE="$1"
+BRANCH="$2"
+BASE=${BASE_BRANCH:-${3:-master}}
+DIR="patches/${4:-$BRANCH}"
+
+usage() {
+ printf "git-review <remote> <branch> [remote-base-branch (def: master)] [patch-dir-name]\n"
+ exit 1
+}
+
+if [ -z "$2" ]; then
+ usage
+fi
+
+
+git fetch origin $BASE
+git fetch "$REMOTE" "$BRANCH"
+
+rm -rf "$DIR"
+mkdir -p "$DIR"
+
+printf "git format-patch origin/%s..FETCH_HEAD -o %s\n" "$BASE" "$DIR"
+git format-patch origin/$BASE..FETCH_HEAD -o "$DIR"
+
+printf "$DIR\n"
+
+read -p "Press enter to review."
+
+bats "$DIR"/*.patch
diff --git a/bin/git-sha256 b/bin/git-sha256
@@ -0,0 +1,4 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i bash -p nix-prefetch-scripts jq
+
+nix-prefetch-git --url "$1" "$@" 2>/dev/null | jq -r .sha256
diff --git a/bin/google-group-subscribe b/bin/google-group-subscribe
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+[ -z "$1" ] && printf "usage: $(basename $0) list-name\n" 1>&2 && exit 1
+
+sendmail -t <<EOF
+From: William Casarin <jb55@jb55.com>
+To: $1+subscribe@googlegroups.com
+EOF
diff --git a/bin/gpginfo b/bin/gpginfo
@@ -0,0 +1 @@
+gpg2 --list-packets $@
diff --git a/bin/grabssh b/bin/grabssh
@@ -0,0 +1,8 @@
+#!/bin/sh
+SSHVARS="SSH_CLIENT SSH_TTY SSH_AUTH_SOCK SSH_CONNECTION DISPLAY"
+
+for x in ${SSHVARS} ; do
+ (eval echo $x=\$$x) | sed 's/=/="/
+ s/$/"/
+ s/^/export /'
+done 1>$HOME/bin/fixssh
diff --git a/bin/group_permissions b/bin/group_permissions
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+DIR="$1"
+GRP="$2"
+if [ -z "$DIR" ] || [ -z "$GRP" ]; then
+ echo "usage: $(basename $0) <dir> <group>"
+ exit 1
+fi
+
+chgrp -R "$GRP" "$DIR"
+chmod -R g+w "$DIR"
+find "$DIR" -type d -exec chmod 2775 {} \;
+find "$DIR" -type f -exec chmod ug+rw {} \;
diff --git a/bin/hackage-docs b/bin/hackage-docs
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -e
+
+if [ "$#" -ne 1 ]; then
+ echo "Usage: scripts/hackage-docs.sh HACKAGE_USER"
+ exit 1
+fi
+
+user=$1
+
+cabal_file=$(find . -maxdepth 1 -name "*.cabal" -print -quit)
+if [ ! -f "$cabal_file" ]; then
+ echo "Run this script in the top-level package directory"
+ exit 1
+fi
+
+pkg=$(awk -F ":[[:space:]]*" 'tolower($1)=="name" { print $2 }' < "$cabal_file")
+ver=$(awk -F ":[[:space:]]*" 'tolower($1)=="version" { print $2 }' < "$cabal_file")
+
+if [ -z "$pkg" ]; then
+ echo "Unable to determine package name"
+ exit 1
+fi
+
+if [ -z "$ver" ]; then
+ echo "Unable to determine package version"
+ exit 1
+fi
+
+echo "Detected package: $pkg-$ver"
+
+dir=$(mktemp -d build-docs.XXXXXX)
+trap 'rm -r "$dir"' EXIT
+
+
+if haddock --hyperlinked-source >/dev/null
+then
+ echo "Using fancy hyperlinked source"
+ HYPERLINK_FLAG="--haddock-option=--hyperlinked-source"
+else
+ echo "Using boring hyperlinked source"
+ HYPERLINK_FLAG="--hyperlink-source"
+fi
+
+cabal haddock --hoogle $HYPERLINK_FLAG --html-location='/package/$pkg-$version/docs' --contents-location='/package/$pkg-$version'
+
+cp -R dist/doc/html/$pkg/ $dir/$pkg-$ver-docs
+
+tar cvz -C $dir --format=ustar -f $dir/$pkg-$ver-docs.tar.gz $pkg-$ver-docs
+
+curl -X PUT \
+ -H 'Content-Type: application/x-tar' \
+ -H 'Content-Encoding: gzip' \
+ -u "$user" \
+ --data-binary "@$dir/$pkg-$ver-docs.tar.gz" \
+ "https://hackage.haskell.org/package/$pkg-$ver/docs"
diff --git a/bin/haskell-emacs b/bin/haskell-emacs
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec nix-shell -Q -p haskellEnv --run 'emacs & disown'
diff --git a/bin/haskell-shell b/bin/haskell-shell
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [$*])"
diff --git a/bin/headers b/bin/headers
@@ -0,0 +1,2 @@
+#!/bin/sh
+head -n1 "${1:-/dev/stdin}" | csv-delim | tr '\t' '\n' | cat -n+
\ No newline at end of file
diff --git a/bin/hex2dec b/bin/hex2dec
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+if [ ! -z "$1" ]; then
+ hash="$1"
+else
+ read -r hash
+fi
+
+<<<"$hash" exec tr '[:lower:]' '[:upper:]' | xargs printf 'ibase=16; %s\n' | bc
diff --git a/bin/hist b/bin/hist
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+import sys
+
+hist = {}
+for line in sys.stdin.readlines():
+ x = line[:-1]
+ if x is '':
+ continue
+ hist[x] = (hist.get(x) or 0) + 1
+
+for key, count in hist.items():
+ print "%s\t%s" % (count, key)
diff --git a/bin/hsp b/bin/hsp
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+[ -z "$1" ] && \
+ printf "usage: cmd | %s txt\n" "$(basename "$0")" 1>&2 && \
+ exit 1
+
+TMP="$(mktemp).$1"
+cat > "$TMP"
+
+hashshare "$TMP"
+
+rm -f "$TMP"
diff --git a/bin/htmlinit b/bin/htmlinit
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+import sys
+
+script_tag = ""
+script = len(sys.argv) > 1 and sys.argv[1] or None
+
+if script:
+ script_tag = '<script src="{}"></script>'.format(script)
+
+
+hello = """
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <title>Hello World</title>
+ </head>
+ <body>
+ <h1>Hello World</h1>
+ {}
+ </body>
+</html>
+""".format(script_tag)
+
+print(hello)
diff --git a/bin/initx b/bin/initx
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -m
+set -e
+pkill clipit || :
+nohup clipit &
+pkill twmnd || :
+nohup twmnd &
+pkill xbindkeys || :
+nohup xbindkeys &
diff --git a/bin/jctlu b/bin/jctlu
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+journalctl --user "$@"
diff --git a/bin/killchrome b/bin/killchrome
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec pkill --oldest chromium
diff --git a/bin/killsession b/bin/killsession
@@ -0,0 +1,2 @@
+#!/bin/sh
+loginctl list-sessions | awk 'NR==2 { print $1 }' | xargs loginctl kill-session
diff --git a/bin/lcli b/bin/lcli
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec $HOME/dev/github/ElementsProject/lightning/cli/lightning-cli --lightning-dir $HOME/.lightning-bitcoin "$@"
diff --git a/bin/lclitn b/bin/lclitn
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+lightning-cli --rpc-file=/home/jb55/.lightning-testnet-rpc "$@"
diff --git a/bin/lessp b/bin/lessp
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+colorize () {
+ chroma -f terminal "$1" | less -R
+}
+
+usage () {
+ printf "usage: lessp file.txt\n" >&2
+ printf " lessp txt < file.txt\n" >&2
+ exit 1
+}
+
+[ $# -eq 0 ] && usage
+
+if [ -t 0 ]; then
+ [ $# -eq 0 ] && usage
+ colorize "$1"
+else
+ [ $# -eq 0 ] && usage
+ ext="$1"
+ tmp="$(mktemp).$ext"
+ cat > "$tmp"
+ colorize "$tmp"
+ rm -f "$tmp"
+fi
diff --git a/bin/lightning-dev b/bin/lightning-dev
@@ -0,0 +1,6 @@
+#!/bin/sh
+exec nix-shell -p \
+ haskellEnv zlib gdb sqlite \
+ autoconf libxml2 asciidoc git clang libtool gmp sqlite autoconf autogen automake \
+ 'python3.withPackages (p: with p; [bitcoinlib pytest_xdist])' \
+ valgrind asciidoc "$@"
diff --git a/bin/lm b/bin/lm
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+find . -type f -printf '%T@ %p\n' | sort -nr | cut -f2- -d" "
diff --git a/bin/ln-channelinfo b/bin/ln-channelinfo
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+lcli listpeers | jq -r ".peers[] | . as \$peer | .channels[] | select(.short_channel_id == \"$1\") as \$channel | \$peer"
diff --git a/bin/ln-forwards b/bin/ln-forwards
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+lcli listforwards | jq -c -r '.forwards[] | [.status,.failreason,.in_msatoshi,.fee,.in_channel,.out_channel] | @tsv' | columnt
diff --git a/bin/ln-nodeinfo b/bin/ln-nodeinfo
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+lcli listnodes | jq ".nodes[] | select(.nodeid == \"$1\")"
diff --git a/bin/load-dark-env b/bin/load-dark-env
@@ -0,0 +1 @@
+export FZF_DEFAULT_OPTS=--color=dark
diff --git a/bin/load-light-env b/bin/load-light-env
@@ -0,0 +1 @@
+export FZF_DEFAULT_OPTS=--color=light
diff --git a/bin/load-theme-env b/bin/load-theme-env
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+[ -z "$1" ] && exit 1
+export FZF_DEFAULT_OPTS=--color="$1"
diff --git a/bin/lock b/bin/lock
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec slock
diff --git a/bin/magnetize b/bin/magnetize
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+echo "magnet:?xt=urn:btih:$1"
diff --git a/bin/makex b/bin/makex
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec chmod +x "$1"
diff --git a/bin/mdr b/bin/mdr
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+files=(readme.md README.md)
+
+for f in "${files[@]}"; do
+ if [ -f "$f" ]; then
+ exec mandown "$f"
+ exit 0
+ fi
+done
+
+if [ -f README ]; then
+ exec less README
+fi
+
+exit 1
diff --git a/bin/mempool-size b/bin/mempool-size
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec bitcoin-cli "$@" getmempoolinfo | jq -r .bytes | nfmt
diff --git a/bin/mount-hdd1 b/bin/mount-hdd1
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+mkdir -p "$HOME/var/razorcx-hdd1"
+exec sudo mount /dev/disk/by-uuid/4828-3568 "$HOME/var/razorcx-hdd1"
diff --git a/bin/my-reboot b/bin/my-reboot
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+vmclose || :
+
+reboot
diff --git a/bin/my-suspend b/bin/my-suspend
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -m
+
+slock &
+
+# pause vms
+if type VBoxManage > /dev/null; then
+ VBoxManage controlvm razoredge pause || :
+fi
+
+systemctl suspend
diff --git a/bin/mydate b/bin/mydate
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec date "$@" '+%a | %b %d | %H:%M:%S | %F'
diff --git a/bin/myip b/bin/myip
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec dig +short myip.opendns.com @resolver1.opendns.com
diff --git a/bin/n b/bin/n
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+out="$("$@" 2>&1)"
+printf "%s\n" "$out"
+exec notify-send "$out"
diff --git a/bin/nannypay b/bin/nannypay
@@ -0,0 +1,129 @@
+#!/usr/bin/env bash
+
+set -e
+
+if [ -z "$NANNY" ]; then
+ printf "set NANNY (and make sure PAY is correct)\n"
+ exit 1
+fi
+
+if [ "$NANNY" == "lisbeth" ]; then
+ pay=18
+fi
+
+if [ "$NANNY" == "diana" ]; then
+ pay=17
+fi
+
+if [ -z $pay ]; then
+ printf "unknown pay for %s\n" "$NANNY"
+ exit 1
+fi
+
+
+CSV="${NANNYPAY_CSV:-$HOME/.local/share/nannypay/${NANNY}.txt}"
+mkdir -p "$(dirname "$CSV")"
+if [ ! -f "$CSV" ]; then
+ touch "$CSV"
+fi
+
+deps() {
+ printf "looks like 'ratio' is broken fam\n" 1>&2
+ exit 3
+}
+
+ratio <<<"0" >/dev/null || deps
+
+cmd="$3"
+
+usage () {
+ printf "usage: nannypay <start-time> <end-time> [r|p]\n" 1>&2
+ show_entries
+ exit 1
+}
+
+duplicate () {
+ printf "there is a already an entry for %s\n" "$1" 1>&2
+ show_entries
+ exit 2
+}
+
+show_entries () {
+ n=${1:-12}
+ printf "\nlast %d entries:\n" "$n" 1>&2
+ (printf "date\tamount\tstart\tend\tminutes\thour_rate\tpaid?\n"; tail -n "$n" "$CSV") | column -t -s $'\t' 1>&2
+ printf "\n%s to be paid\n" "$(to_pay_amt)" 1>&2
+}
+
+
+do_pay () {
+ sed -i"" "/\*$/! s,$,\t\*,g" "$CSV"
+}
+
+
+
+to_pay_amt () {
+ dates=${1:-$(grep -v '\*$' "$CSV")}
+ <<<"$dates" \
+ cut -d $'\t' -f2 \
+ | paste -sd+ \
+ | ratio \
+ | bc -l \
+ | xargs printf "%.02f\n"
+}
+
+pay() {
+ dates=$(grep -v '\*$' "$CSV")
+ amt=$(to_pay_amt "$dates")
+
+ if [ "$amt" = "0.00" ]; then
+ printf "nothing to pay!\n" 1>&2
+ else
+ printf "paying amt %s from dates:\n%s\ncontinue? (y/n)\n" "$amt" "$dates" 1>&2
+
+ read -r confirm
+
+ if [ "$confirm" = "y" ]; then
+ do_pay "$amt"
+ else
+ printf "cancelled.\n" 1>&2
+ fi
+ fi
+}
+
+if [ -z "$1" ] && [ -z "$2" ]
+then
+ usage
+fi
+
+get_minutes() {
+ datediff "$1" "$2" -f %M
+}
+
+get_amt() {
+ <<<"$1" xargs printf "%s/(60/$pay)\n" | ratio
+}
+
+if [ "$cmd" = "r" ]; then
+ start="$1"
+ end="$2"
+ minutes=$(get_minutes "$start" "$end")
+ amt=$(get_amt "$minutes")
+ day=${4:-$(date +%F)}
+ start="$1"
+ end="$2"
+
+ printf "%s\n" "$amt"
+ #grep "$day" "$CSV" >/dev/null && duplicate "$day"
+ printf "%s\t%s\t%s\t%s\t%s\t%s\n" "$day" "$amt" "$start" "$end" "$minutes" "$pay" >> "$CSV"
+elif [ "$1" = "p" ] || [ "$cmd" = "p" ]; then
+ pay
+elif [ "$1" = "e" ]; then
+ "$EDITOR" "$CSV"
+else
+ minutes=$(get_minutes "$1" "$2")
+ amt=$(get_amt "$minutes")
+ printf "%s\n" "$amt"
+fi
+
+show_entries
diff --git a/bin/netbps b/bin/netbps
@@ -0,0 +1,23 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use Time::HiRes;
+
+my $reporting_interval = 10.0; # seconds
+my $bytes_this_interval = 0;
+my $start_time = [Time::HiRes::gettimeofday()];
+
+STDOUT->autoflush(1);
+
+while (<>) {
+ if (/ length (\d+):/) {
+ $bytes_this_interval += $1;
+ my $elapsed_seconds = Time::HiRes::tv_interval($start_time);
+ if ($elapsed_seconds > $reporting_interval) {
+ my $bps = $bytes_this_interval / $elapsed_seconds;
+ printf "%02d:%02d:%02d %10.2f Bps\n", (localtime())[2,1,0],$bps;
+ $start_time = [Time::HiRes::gettimeofday()];
+ $bytes_this_interval = 0;
+ }
+ }
+}
diff --git a/bin/netscan b/bin/netscan
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+INTERFACE=${1:-$(ifconfig | grep ^e | cut -d":" -f1 | head -n1)}
+echo "using interface $INTERFACE"
+IPRANGE=$(ifconfig $INTERFACE | grep inet\ | cut -d" " -f6 | cut -d"." -f1-3)".1-255"
+echo "scanning $IPRANGE for open ports"
+CMD="sudo nmap -Pn --top-ports=10 --open $IPRANGE"
+echo "running: $CMD"
+$CMD
diff --git a/bin/netscan.sh b/bin/netscan.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+INTERFACE=${1:-$(ifconfig | grep ^e | cut -d":" -f1 | head -n1)}
+echo "using interface $INTERFACE"
+IPRANGE=$(ifconfig $INTERFACE | grep inet\ | cut -d" " -f6 | cut -d"." -f1-3)".1-255"
+echo "scanning $IPRANGE for open ports"
+CMD="sudo nmap -Pn --top-ports=10 --open $IPRANGE"
+echo "running: $CMD"
+$CMD
diff --git a/bin/newlines b/bin/newlines
@@ -0,0 +1,3 @@
+#!/bin/sh
+awk 'FNR == NR { oldfile[$0]=1; };
+ FNR != NR { if(oldfile[$0]==0) print; }'
diff --git a/bin/nfmt b/bin/nfmt
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec numfmt --to=iec
diff --git a/bin/nix-build-cache b/bin/nix-build-cache
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+printf "building cache of nixpkgs\n" >&2
+nix-env -f $NIXPKGS -qaP \* > /tmp/search1 &
+printf "building cache of haskellPackages\n" >&2
+nix-env -f $NIXPKGS -qaPA haskellPackages > /tmp/search2 &
+wait
+cat /tmp/search1 /tmp/search2 | sort > ~/.nixenv.cache
+rm -f /tmp/search1 /tmp/search2
diff --git a/bin/nix-cabal-build b/bin/nix-cabal-build
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+cabal=$(echo *.cabal)
+
+if [[ ! -f default.nix ]]; then
+ cabal2nix --sha256=X ./$cabal > default.nix
+ sed -i 's#sha.*#src = builtins.filterSource (path: type: type != "unknown") ./.;#' default.nix
+fi
+
+if [[ ! -f Setup.hs && ! -f Setup.lhs ]]; then
+ cat > Setup.hs <<EOF
+import Distribution.Simple
+main = defaultMain
+EOF
+fi
+
+nix-build "$@" -E 'let pkgs = import <nixpkgs> {}; in pkgs.stdenv.lib.callPackageWith (pkgs // pkgs.haskellngPackages // pkgs.haskellPackages) ./default.nix {}'
diff --git a/bin/nix-cabal-shell b/bin/nix-cabal-shell
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+cabal=$(echo *.cabal)
+
+if [[ ! -f default.nix ]]; then
+ cabal2nix --sha256=X ./$cabal > default.nix
+ sed -i 's#sha.*#src = builtins.filterSource (path: type: type != "unknown") ./.;#' default.nix
+fi
+
+if [[ ! -f Setup.hs ]]; then
+ cat > Setup.hs <<EOF
+import Distribution.Simple
+main = defaultMain
+EOF
+fi
+
+nix-shell -E 'let pkgs = import <nixpkgs> {}; in pkgs.stdenv.lib.callPackageWith (pkgs // pkgs.haskellngPackages) ./default.nix {}' "$@"
diff --git a/bin/nix-cp-hash b/bin/nix-cp-hash
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+grep r:sha256 | cut -d" " -f 7 | sed 's,^‘,,;s,’$,,' | tr -d '\n' | tee /dev/tty | xclip
diff --git a/bin/nix-deps b/bin/nix-deps
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+exec nix-store -q --references "$@"
diff --git a/bin/nix-eval b/bin/nix-eval
@@ -0,0 +1,37 @@
+#/usr/bin/env bash
+
+usage () {
+ echo "nix-eval [--nixpkgs|-n] <expr>"
+ echo ""
+ echo " examples"
+ echo ""
+ echo " \$ nix-eval '1+1'"
+ echo " 2"
+ echo ""
+ echo " \$ nix-eval -n '\"\${pkgs.xlibs.xset}\"'"
+ echo " \"/nix/store/6n5f894ndps4rnrvdx8z95sw4pmd1989-xset-1.2.3\""
+ echo ""
+ exit 1
+}
+
+prelude=""
+
+[ "$#" -eq 0 ] && usage
+
+while [ "$#" -gt 1 ]; do
+ i="$1"; shift 1
+ case "$i" in
+ --help)
+ usage
+ ;;
+ --nixpkgs|-n)
+ prelude="with import <nixpkgs> {}; "
+ ;;
+ *)
+ echo "$0: unknown option \`$i'"
+ exit 1
+ ;;
+ esac
+done
+
+nix-instantiate --eval --expr "${prelude}$@"
diff --git a/bin/nix-grep b/bin/nix-grep
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+if [ ! -f $HOME/.nixenv.cache ]; then
+ >&2 echo "cache not found, generating..."
+ nix-build-cache
+ >&2 echo "done"
+ >&2 echo ""
+fi
+
+grep -i $@ $HOME/.nixenv.cache
diff --git a/bin/nix-install b/bin/nix-install
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec nix-env -f $NIXPKGS -Q --max-jobs 4 -k -iA "$@"
diff --git a/bin/nix-lib-path b/bin/nix-lib-path
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+exec nix-instantiate --eval --expr \
+ "with import <nixpkgs> {}; lib.makeLibraryPath (with pkgs; [ $* ])" | sed 's/"//g'
diff --git a/bin/nix-pkg b/bin/nix-pkg
@@ -0,0 +1,17 @@
+#!/usr/bin/env sh
+cmd="$1"
+
+usage () {
+ echo "nix-pkg <build|shell|instantiate> [OPTIONS]..."
+ exit 1
+}
+
+[[ "$cmd" = "build" ||
+ "$cmd" = "shell" ||
+ "$cmd" = "instantiate"
+]] || usage
+
+shift
+
+nix-${cmd} -E 'with import <nixpkgs> {}; callPackage ./default.nix {}' $@
+
diff --git a/bin/nix-revdep b/bin/nix-revdep
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec nix-store --query --referrers "$@"
diff --git a/bin/nix-src b/bin/nix-src
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+nix-instantiate --eval --expr "with import <nixpkgs> {}; pkgs.$1.src.urls" \
+ | sed 's,\[ ",,;s," \]$,,'
diff --git a/bin/nixhash b/bin/nixhash
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+grep r:sha256 | cut -d" " -f 7 | sed 's,^‘,,;s,’$,,' | tr -d '\n' | tee | xclip
diff --git a/bin/nostat b/bin/nostat
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+services="home-email-notifier work-email-notifier email-notify-switcher"
+
+export SYSTEMD_COLORS=1
+
+ctl () {
+ printf "%s-ing %s email notifier\n" "$1" "$2" >&2
+}
+
+if [ $# -eq 0 ]
+then
+ printf "nostat [ARGS].. - ARGS: {+,-}h, +start, -stop\n" >&2
+fi
+
+for var in "$@"
+do
+ case $var in
+ +h)
+ ctl start email-fetcher
+ touch ~/var/notify/home
+ ;;
+ -h)
+ ctl stop email-fetcher
+ rm -f ~/var/notify/home
+ ;;
+ esac
+done
+
+printf '\n' >&2
+
+res=$(ls -1 ~/var/notify)
+if [ -z "$res" ]
+then
+ res=off
+fi
+
+printf '%s\n' "$res"
diff --git a/bin/notmuch-personal b/bin/notmuch-personal
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec notmuch "$@"
diff --git a/bin/notmuch-poll b/bin/notmuch-poll
@@ -0,0 +1,239 @@
+#!/usr/bin/env sh
+
+notmuchcmd () {
+ echo "notmuch" "$@"
+ $(notmuch "$@")
+}
+
+# sent
+notmuchcmd tag +sent tag:inbox and not tag:sent and folder:".Sent"
+
+# youtube
+notmuchcmd tag +youtube tag:inbox and not tag:youtube and folder:".YouTube"
+notmuchcmd tag +talk from:"Talks at Google" and tag:youtube and tag:inbox and not tag:talk
+notmuchcmd tag +lecture from:"Khan Academy" and tag:youtube and tag:inbox and not tag:lecture
+
+# lobsters, ycombinator
+notmuchcmd tag +lobsters +rss +tech +busy tag:inbox and not tag:lobsters and folder:".Lists.lobsters"
+notmuchcmd tag +hn +rss +tech +busy tag:inbox and not tag:hn and folder:".HackerNews"
+
+
+# promote from busy if its interesting
+notmuchcmd tag -busy +haskell tag:inbox and not tag:haskell and '(tag:lobsters or tag:hn)' and subject:haskell
+notmuchcmd tag -busy +icn tag:inbox and not tag:icn and '(tag:lobsters or tag:hn)' and subject:icn
+notmuchcmd tag -busy +ndn tag:inbox and not tag:ndn and '(tag:lobsters or tag:hn)' and subject:ndn
+notmuchcmd tag -busy +ccnx tag:inbox and not tag:ccnx and '(tag:lobsters or tag:hn)' and subject:ccnx
+notmuchcmd tag -busy +cicn tag:inbox and not tag:cicn and '(tag:lobsters or tag:hn)' and subject:cicn
+notmuchcmd tag -busy +nix tag:inbox and not tag:nix and '(tag:lobsters or tag:hn)' and subject:nix
+notmuchcmd tag -busy +spacemacs tag:inbox and not tag:spacemacs and '(tag:lobsters or tag:hn)' and subject:spacemacs
+notmuchcmd tag +list tag:inbox and to:"groups.io"
+notmuchcmd tag +pony +pl tag:inbox and to:"pony.groups.io"
+notmuchcmd tag +emacs +emacs-dev +list tag:inbox and folder:".Lists.emacs"
+notmuchcmd tag +guix +list tag:inbox and folder:".Lists.guix"
+notmuchcmd tag +arxiv +busy tag:inbox and not tag:arxiv and folder:".Arxiv"
+notmuchcmd tag +ml tag:inbox and not tag:ml and tag:arxiv and subject:"stat.ML"
+
+# notmuch stuff
+notmuchcmd tag +francesc from:elies@posteo.net or from:francesc.elies@gmail.com and not tag:francesc
+notmuchcmd tag +notmuch +list to:notmuch and tag:inbox and not tag:notmuch
+
+# rss
+notmuchcmd tag +rss tag:inbox and not tag:rss and folder:".RSS"
+notmuchcmd tag +rss +reddit +busy tag:inbox and not tag:reddit and folder:".Reddit"
+notmuchcmd tag +best +haskell \
+ tag:inbox and \
+ from:"A Neighborhood of Infinity" or \
+ from:"Haskell for all" or \
+ from:"Shtetl-Optimized" or \
+ from:"Lost in Technopolis"
+
+notmuchcmd tag +baez +best from:"John Baez" or from:Baez_J and not tag:baez
+notmuchcmd tag +best +arxiv from:"ArXiv Query" and not tag:best and tag:inbox
+notmuchcmd tag +best +tech from:"Technology Review" and not tag:best
+notmuchcmd tag +best +physics +compsci from:from:"Shtetl-Optimized" and not tag:best
+notmuchcmd tag +best +haskell +oleg from:"okmij" and not tag:best
+notmuchcmd tag +best +edge from:"edge_manager" and not tag:best
+
+notmuchcmd tag +nix \
+ tag:inbox and \
+ not tag:nix and \
+ from:"Newest questions tagged nix - Stack Overflow" or \
+ from:"Thoughts about computer technologies" or \
+ from:"NixOS Planet"
+
+notmuchcmd tag +busy +emacs +so +question \
+ tag:inbox and \
+ not tag:emacs and \
+ from:"Emacs Stack Exchange"
+
+notmuchcmd tag +busy +bitcoin +so +question \
+ tag:inbox and \
+ not tag:so and \
+ from:"Bitcoin Stack Exchange"
+
+notmuchcmd tag +best -busy -filed \
+ tag:inbox and \
+ not tag:best and \
+ \(from:"Pieter Wuille" or \
+ from:"Peter Todd" or \
+ from:"Adam Back" or \
+ from:"Gregory Maxwell" or \
+ from:roconnor \
+ \)
+
+notmuchcmd tag +elm \
+ tag:inbox and \
+ not tag:elm and \
+ from:"Newest questions tagged elm"
+
+notmuchcmd tag +rust \
+ tag:inbox and \
+ not tag:rust and \
+ from:rust
+
+notmuchcmd tag +emacs \
+ tag:inbox and \
+ not tag:emacs and \
+ from:emacs
+
+notmuchcmd tag +busy +question +so \
+ tag:inbox and \
+ not tag:so and \
+ from:"Stack Overflow"
+
+notmuchcmd tag +elec +busy tag:inbox and not tag:elec and from:"Adafruit Industries"
+notmuchcmd tag +python tag:inbox and not tag:python and from:"Neopythonic"
+notmuchcmd tag +physics \
+ tag:inbox and \
+ not tag:physics and \
+ from:"Physics and cake" or \
+ from:"symmetry magazine"
+
+notmuchcmd tag +ml +best -busy tag:inbox and not tag:ml and from:"Andrej Karpathy"
+notmuchcmd tag +ml tag:inbox and not tag:ml and from:"Machine Learning"
+
+notmuchcmd tag +haskell \
+ tag:inbox and \
+ not tag:haskell and \
+ from:"Bartosz Milewski" or \
+ from:"Haskell" or \
+ from:"Declarative Languages Blog"
+
+notmuchcmd tag +gamedev tag:inbox and not tag:gamedev and from:"Unity Technologies Blog"
+notmuchcmd tag +prog +lisp tag:inbox and not tag:prog and from:"Peter Norvig"
+notmuchcmd tag +startup tag:inbox and not tag:startup and from:"Paul Graham"
+notmuchcmd tag +talk tag:inbox and not tag:talk and from:"Video Lectures"
+notmuchcmd tag +js tag:inbox and not tag:js and from:"Vjeux"
+notmuchcmd tag +cli +tips tag:inbox and not tag:cli and from:"UNIX Command Line"
+notmuchcmd tag +go tag:inbox and not tag:go and from:"Planet 5"
+notmuchcmd tag +busy +hack tag:inbox and not tag:hack and from:"Hackaday"
+notmuchcmd tag +vim tag:inbox and not tag:hack and from:"Vimcasts"
+notmuchcmd tag +comic tag:inbox and not tag:comic and from:"xkcd"
+notmuchcmd tag +lesswrong +rationality tag:inbox and not tag:lesswrong and from:"Less Wrong"
+notmuchcmd tag +rationality tag:inbox and \(from:"Slate Star Codex" \
+ or from:"Overcoming Bias" \
+ or from:"Information Processing"\)
+
+# me
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:jackbox55@gmail.com
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:jb55@jb55.com
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:jb@jb55.com
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:bill@monstercat.com
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:bill@monster.cat
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:bill@casarin.me
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:bill@casarin.ca
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:will@casarin.ca
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:will@casarin.me
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:william@casarin.ca
+notmuchcmd tag +to-me not tag:to-me and not tag:rss and tag:inbox and to:william@casarin.me
+
+notmuchcmd tag +flagged tag:inbox and not tag:flagged and to:jackbox55+star@gmail.com
+notmuchcmd tag +flagged tag:inbox and not tag:flagged and to:jackbox55+s@gmail.com
+
+# annoying
+notmuchcmd tag +alert tag:inbox and folder:".Alerts"
+notmuchcmd tag +update tag:inbox and folder:".Update"
+notmuchcmd tag +alert +circleci tag:inbox and from:builds@circleci.com
+notmuchcmd tag +alert +trello tag:inbox and from:trello
+notmuchcmd tag +alert +sentry tag:inbox and from:noreply@md.getsentry.com or from:noreply@outbound.getsentry.com
+
+# work
+notmuchcmd tag +monstercat tag:inbox and to:bill@monstercat.com
+notmuchcmd tag +monstercat +connect tag:inbox and to:monstercat/connect
+
+
+# forums
+notmuchcmd tag +patchwork +list to:patchwork@lists.ozlabs.org and tag:inbox
+notmuchcmd tag +wayland +list to:wayland-devel and tag:inbox and not tag:wayland
+notmuchcmd tag +suckless +list to:suckless.org and tag:inbox and not tag:suckless
+notmuchcmd tag +ats +list to:ats-lang-users and tag:inbox
+notmuchcmd tag +bitcoin +list folder:".Lists.bitcoin" and tag:inbox
+notmuchcmd tag +bitcoin +libbitcoin +list to:libbitcoin@lists.dyne.org and tag:inbox
+notmuchcmd tag +bitcoin +core +busy to:bitcoin@noreply.github.com and tag:inbox
+notmuchcmd tag +lightning +list to:lightning-dev@lists.inuxfoundation.org and tag:inbox
+notmuchcmd tag +lightning to:"lightningnetwork/" and tag:inbox
+notmuchcmd tag +lightning +clightning to:"ElementsProject/lightning" and tag:inbox
+notmuchcmd tag +cabal2nix +nix +list to:NixOS/cabal2nix and tag:inbox
+notmuchcmd tag +cicn +icn +list to:cicn and tag:inbox
+notmuchcmd tag +component +list to:componentjs.googlegroups.com and tag:inbox
+notmuchcmd tag +craigslist +list to:reply.craigslist.org and tag:inbox
+notmuchcmd tag +crypto +list to:cryptography.metzdowd.com and tag:inbox
+notmuchcmd tag +elm +list folder:".Lists.elm" and tag:inbox
+notmuchcmd tag +github +list folder:".GitHub" and tag:inbox
+notmuchcmd tag +haskell +cabal-dev +list to:cabal-devel.haskell.org and tag:inbox
+notmuchcmd tag +haskell +commercial-haskell +list to:commercialhaskell.googlegroups.com and tag:inbox
+notmuchcmd tag +haskell +ghc-devs +list to:ghc-devs.haskell.org and tag:inbox
+notmuchcmd tag +haskell +haskell-cafe +list to:haskell-cafe.haskell.org and tag:inbox
+notmuchcmd tag +haskell +haskell-libraries +list to:libraries.haskell.org or to:libraries@haskell.org and tag:inbox
+notmuchcmd tag +haskell +list to:haskell.haskell.org and tag:inbox
+notmuchcmd tag +haskell +pipes +list to:haskell-pipes.googlegroups.com and tag:inbox
+notmuchcmd tag +haskell +streaming to:streaming-haskell.googlegroups.com and tag:inbox
+notmuchcmd tag +icn +list folder:".Lists.icn" and tag:inbox
+notmuchcmd tag +ndn +list to:ndn-interest and tag:inbox
+notmuchcmd tag +nix-dev +nix +list \(to:nix-dev@lists.science.uu.nl OR to:nix-dev@cs.uu.nl OR to:nix-devel@googlegroups.com\) and tag:inbox
+notmuchcmd tag +nixpkgs +nix +busy +list to:nixpkgs@noreply.github.com and tag:inbox
+notmuchcmd tag +nixpm +nix +list to:nix@noreply.github.com and tag:inbox
+notmuchcmd tag +hydra +nix +list to:hydra@noreply.github.com and tag:inbox
+notmuchcmd tag +otr +list to:otr-users.lists.cypherpunks.ca and tag:inbox
+notmuchcmd tag +redo +list to:redo-list.googlegroups.com and tag:inbox
+notmuchcmd tag +shen +list to:qilang and tag:inbox
+notmuchcmd tag +spacemacs +busy +list to:spacemacs@noreply.github.com and tag:inbox
+notmuchcmd tag +webvr +vr +list to:web-vr-discuss and tag:inbox
+
+
+# except if someone mentions me
+notmuchcmd tag +flagged -busy tag:inbox and tag:list and \(jb55 or tag:to-me\)
+
+# filed
+notmuchcmd tag +filed '(tag:list or tag:rss or tag:busy)' and not tag:filed and not tag:best and not tag:flagged and tag:inbox
+
+# remove annoying from inbox, should be last in file
+notmuchcmd tag -inbox tag:inbox and tag:alert or tag:update
+
+notmuchcmd tag -inbox +newsletter \
+ tag:inbox and \
+ from:newsletters.microsoft.com or \
+ from:freescale
+
+notmuchcmd tag -inbox \
+ tag:inbox and \
+ from:philphys.phil.elte.hu or \
+ from:everything-list.googlegroups.com or \
+ from:codesite-noreply@google.com or \
+ from:getsatisfaction.com or \
+ from:post@tinyportal.net or \
+ from:yahoo.com.hk
+
+notmuchcmd tag -inbox +brandalliance tag:inbox and from:mail@brandalliancelounge.com
+notmuchcmd tag -inbox +spam tag:inbox and folder:".Spam"
+
+# k9mail
+notmuchcmd tag -inbox tag:inbox and folder:".Archive"
+
+# thunderbird I guess?
+notmuchcmd tag -inbox tag:inbox and folder:".Archives.2017"
+
+# remove to-me from rss items
+notmuchcmd tag -to-me tag:inbox and tag:rss and tag:to-me
+
+printf "notmuch tagging done.\n"
diff --git a/bin/notmuch-remote b/bin/notmuch-remote
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+printf -v ARGS "%q " "$@"
+exec ssh charon notmuch $ARGS
diff --git a/bin/notmuch-update-mcat b/bin/notmuch-update-mcat
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+notmuchcmd () {
+ echo "notmuch" "$@"
+ $(/home/jb55/bin/notmuch-work "$@")
+}
+
+# util
+notmuchcmd tag +to-me to:bill@monstercat.com and tag:inbox and not tag:to-me
+notmuchcmd tag +from-me from:bill@monstercat.com and tag:inbox and not tag:from-me
+
+# labels
+notmuchcmd tag +filed +royalties '(from:royalties@monstercat.com or to:royalties@monstercat.com)' and tag:inbox and not tag:royalties
+
+# noise
+notmuchcmd tag +events +noise to:events@monstercat.com and tag:inbox and not tag:events
+notmuchcmd tag +sentry +noise +error from:noreply@md.getsentry.com and tag:inbox and not tag:sentry
+notmuchcmd tag +bugsnag +noise +error from:bugsnag.com and tag:inbox and not tag:bugsnag
+notmuchcmd tag +slack +update +noise from:feedback@slack.com and tag:inbox and not tag:slack
+notmuchcmd tag +sendgrid +update +noise from:sendgrid.com and tag:inbox and not tag:sendgrid
+notmuchcmd tag +dev +clubhouse +update from:robot@clubhouse.io and tag:inbox and not tag:clubhouse
+notmuchcmd tag +noise +update from:"Clubhouse" and tag:inbox and not tag:clubhouse
+notmuchcmd tag +itunes +noise +update subject:"iTunes Weekly Upload Report" and tag:inbox and not tag:itunes
+notmuchcmd tag +noise subject:"spam report"
+
+# important stuff
+notmuchcmd tag +error from:systemd and subject:Failed and not tag:systemd and tag:inbox
+notmuchcmd tag +dev +github from:notifications@github.com and tag:inbox and not tag:github
+notmuchcmd tag +internal from:monstercat.com and not tag:internal and tag:inbox
+
+# notmuchcmd tag +flagged \
+# tag:internal and \
+# tag:to-me and \
+# not tag:flagged and \
+# not tag:noise and \
+# not tag:from-me and tag:inbox
+
+# alerts
+notmuchcmd tag +flagged from:UPS or \(from:ari and from:monstercat\)
diff --git a/bin/notmuch-update-personal b/bin/notmuch-update-personal
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec muchsync -C ~/.notmuch-config-personal notmuch
diff --git a/bin/notmuch-work b/bin/notmuch-work
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec notmuch --config "$HOME/.notmuch-config-work" "$@"
diff --git a/bin/npmrun b/bin/npmrun
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+PATH="$(npm bin):$PATH" "$@"
diff --git a/bin/nsr b/bin/nsr
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+cmd="$1"
+shift
+nix-shell -p "$cmd" --run "$@"
diff --git a/bin/open b/bin/open
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec $HOME/bin/xdg-open "$@"
diff --git a/bin/open-dl b/bin/open-dl
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "${1:-$HOME/Downloads}"
+
+out=$(find . -maxdepth 1 -name "*.${2:-pdf}" -type f -printf "%T@ %Tc %p\n" \
+ | sort -nr \
+ | cut -f2- -d" " \
+ | dmenu -p docs -l 30 \
+ | cut -f3- -d" ")
+
+if [ -z "$out" ]; then
+ exit 1
+fi
+
+open "$out"
diff --git a/bin/otp b/bin/otp
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+res=$(< $HOME/authy dmenu -i -p 'authy' -l 7)
+
+
+IFS=, read -r label key site digits <<<"$res"
+
+#printf '%s %s\n' "$site" "$label" >&2
+oathtool -d "$digits" --totp -b "$key"
diff --git a/bin/ots-git b/bin/ots-git
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+# Wrapper for the ots-git-gpg-wrapper
+#
+# Required because git's gpg.program option doesn't allow you to set command
+# line options; see the doc/git-integration.md
+
+if [ -n "$GIT_OTS" ] && [ "$GIT_OTS" -eq 1 ]; then
+ exec ots-git-gpg-wrapper --gpg-program /run/current-system/sw/bin/gpg -- "$@"
+else
+ exec /run/current-system/sw/bin/gpg "$@"
+fi
+
diff --git a/bin/parallel-chunked b/bin/parallel-chunked
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf "%s <file> <process-script>\n\n" "$0" >&2
+ printf "splits a file into chunks and process it line by line in parallel\n" >&2
+ exit 1
+}
+
+if [ -z "$1" ]; then
+ usage
+fi
+
+outfile="$1"
+shift
+
+blocksize=$(bc <<<"$(stat -c %s "$outfile") / $(nproc)")
+printf 'parallel with %s chunks...\n' "$(numfmt --to=iec "$blocksize")" >&2
+exec parallel -k --pipepart --block "$blocksize" -a "$outfile" "$@"
diff --git a/bin/pdf2remarkable b/bin/pdf2remarkable
@@ -0,0 +1,156 @@
+#!/usr/bin/env bash
+# Transfer PDF file(s) to a reMarkable
+# Adrian Daerr 2017/2018 - public domain
+#
+# - The files will appear in reMarkable's top-level "My Files" directory,
+# - After finishing all transfers, you have to restart the xochitl
+# service on the tablet in order to force a scan of its document
+# directory ${xochitldir} (so that you see the newly transferred
+# files), e.g. by sending the tablet the following command:
+# ssh remarkable systemctl restart xochitl
+#
+# Disclaimer and liability limitation:
+# [see also all-caps text borrowed from GPL below]
+# - This is a dirty hack based on superficial reverse-engineering.
+# - Expect this script to break at any time, especially upon a
+# reMarkable system upgrade
+# - I am not responsible for any damage caused by this script,
+# including (but not limited to) bricking your reMarkable, erasing
+# your documents etc. YOU ARE USING THIS SOFTWARE ON YOUR OWN RISK.
+#
+# Disclaimer of Warranty.
+#
+# THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+# APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE
+# COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+# “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE
+# RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.
+# SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+# NECESSARY SERVICING, REPAIR OR CORRECTION.
+#
+# Limitation of Liability.
+#
+# IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+# WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO
+# MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE
+# LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
+# INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
+# INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+# DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+# YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE
+# WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS
+# BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+#
+# Prerequisites:
+#
+# * The ssh access has to be configured under the host alias 'remarkable',
+# e.g. by putting the following in .ssh/config :
+# | host remarkable
+# | Hostname 10.11.99.1
+# | User root
+# | ForwardX11 no
+# | ForwardAgent no
+# See also the variable "xochitldir" below
+#
+# * Beyond core utilities (date, basename,...), the following software
+# has to be installed on the host computer:
+# - uuidgen
+# - imagemagick (or graphicsmagick)
+
+set -e
+
+# This is where ssh will try to copy the files associated with the document
+xochitldir=remarkable:.local/share/remarkable/xochitl/
+
+# Check if we have something to do
+if [ $# -lt 1 ]; then
+ echo "Transfer PDF document to a reMarkable tablet"
+ echo "usage: $(basename $0) path-to-pdf-file [path-to-pdf-file]..."
+ exit 1
+fi
+
+# Create directory where we prepare the files as the reMarkable expects them
+tmpdir=$(mktemp -d)
+
+# Loop over the command line arguments,
+# which we expect are paths to the PDF files to be transfered
+for pdfname in "$@" ; do
+
+# reMarkable documents appear to be identified by universally unique IDs (UUID),
+# so we generate one for the document at hand
+uuid=$(uuidgen)
+
+# Copy the PDF file itself
+cp "$pdfname" "${tmpdir}/${uuid}.pdf"
+
+# Add metadata
+# The lastModified item appears to contain the date in milliseconds since Epoch
+cat <<EOF >>"${tmpdir}/${uuid}.metadata"
+{
+ "deleted": false,
+ "lastModified": "$(date +%s)000",
+ "metadatamodified": false,
+ "modified": false,
+ "parent": "",
+ "pinned": false,
+ "synced": false,
+ "type": "DocumentType",
+ "version": 1,
+ "visibleName": "$(basename "$pdfname" .pdf)"
+}
+EOF
+
+# Add content information
+cat <<EOF >"${tmpdir}/${uuid}.content"
+{
+ "extraMetadata": {
+ },
+ "fileType": "pdf",
+ "fontName": "",
+ "lastOpenedPage": 0,
+ "lineHeight": -1,
+ "margins": 100,
+ "pageCount": 1,
+ "textScale": 1,
+ "transform": {
+ "m11": 1,
+ "m12": 1,
+ "m13": 1,
+ "m21": 1,
+ "m22": 1,
+ "m23": 1,
+ "m31": 1,
+ "m32": 1,
+ "m33": 1
+ }
+}
+EOF
+
+# Add cache directory
+mkdir "${tmpdir}/${uuid}.cache"
+
+# Add highlights directory
+mkdir "${tmpdir}/${uuid}.highlights"
+
+# Add thumbnails directory
+mkdir "${tmpdir}/${uuid}.thumbnails"
+
+# Generate preview thumbnail for the first page
+# Different sizes were found (possibly depending on whether created by
+# the reMarkable itself or some synchronization app?): 280x374 or
+# 362x512 pixels. In any case the thumbnails appear to be baseline
+# jpeg images - JFIF standard 1.01, resolution (DPI), density 228x228
+# or 72x72, segment length 16, precision 8, frames 3
+#
+# The following will look nice only for PDFs that are higher than about 32mm.
+#convert -limit thread 1 -density 300 "$pdfname"'[0]' -colorspace Gray -separate -average -shave 5%x5% -resize 280x374 "${tmpdir}/${uuid}.thumbnails/0.jpg"
+
+# Transfer files
+echo "Transferring $pdfname$ as $uuid$"
+scp -r "${tmpdir}"/* ${xochitldir}
+rm -rf "${tmpdir:?}"/*
+done
+
+rm -rf "${tmpdir}"
diff --git a/bin/pdfcat b/bin/pdfcat
@@ -0,0 +1,3 @@
+#!/bin/sh
+printf 'writing to out.pdf\n' 1>&2
+exec gs -dNOPAUSE -sDEVICE=pdfwrite -sOUTPUTFILE=out.pdf -dBATCH "$@"
diff --git a/bin/pdfnow b/bin/pdfnow
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+#PANDOC='pandoc --metadata=fontfamily:libertine'
+PANDOC='pandoc --metadata=fontfamily:libertine -V geometry:margin=1.2in --variable urlcolor=cyan'
+
+usage () {
+ printf "usage: pdfnow file.md OR <file.md pdfnow markdown\n" >&2
+ exit 1
+}
+
+[ $# -eq 0 ] && usage
+
+if [ -t 0 ]; then
+ [ $# -eq 0 ] && usage
+ $PANDOC "$@" -o /tmp/out.pdf
+else
+ [ $# -eq 0 ] && usage
+ ext="$1"
+ shift
+ $PANDOC "$@" -f "$ext" -o /tmp/out.pdf
+fi
+
+zathura /tmp/out.pdf &>/dev/null
diff --git a/bin/phlogs b/bin/phlogs
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec sacc gopher://gopher.black/1/moku-pona
diff --git a/bin/phone-batt b/bin/phone-batt
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "Phone $(phonectl batt)"
diff --git a/bin/phone-clipboard b/bin/phone-clipboard
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -e
+
+send_to_phone() {
+ phonectl copy:"$@"
+ notify-send "copied '$@' to phone"
+ exit 0
+}
+
+if [ ! -z "$@" ]
+then
+ send_to_phone "$@"
+fi
+
+clipboard=$(phonectl clipboard)
+
+if [[ "$clipboard" = "GPhone clipboard: "* ]]; then
+ <<<"$clipboard" sed "s/^GPhone clipboard: //g"
+ exit 0
+fi
+
+printf "%s\n" "$clipboard" >&2
+notify-send "could not access phone clipboard"
+exit 1
diff --git a/bin/phonectl b/bin/phonectl
@@ -0,0 +1,6 @@
+#!/bin/sh
+[ -z "$1" ] && exit 1
+resp=$(echo "$1" | nc -U /tmp/phonectl.sock | jq -r .response)
+[ "$resp" = "null" ] && exit 0
+echo "$resp"
+exit 0
diff --git a/bin/postjson b/bin/postjson
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+[ -z "$1" ] && printf "usage: %s <url> [curl_opts...] < jsondata\n" $(basename $0) && exit 1
+
+url="$1"
+shift
+
+curl -sL -X POST -H 'Content-Type: application/json' -d @- "$@" "$url"
diff --git a/bin/prettysexp b/bin/prettysexp
@@ -0,0 +1,16 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i racket -p racket
+#lang racket
+
+(require racket/pretty)
+
+(define (pretty-write-all)
+
+ (define next (read))
+
+ (when (not (eof-object? next))
+ (pretty-write next)
+ (pretty-write-all)))
+
+
+(pretty-write-all)
diff --git a/bin/proquint-ip b/bin/proquint-ip
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf "proquint-ip <ip-address or proquint>"
+}
+
+if [ -z $1 ]
+then usage
+ exit 1
+fi
+
+input="$1"
+is_ip=0
+
+if [[ $1 =~ ^[0-9] ]]
+then
+ is_ip=1
+ input=$(<<<"$1" sed 's,\., ,g' | xargs printf 'x%02x%02x%02x%02x\n')
+fi
+
+output=$(proquint "$input")
+
+if [[ is_ip -eq 0 ]]
+then
+ <<<"$output" sed 's,^x,,;s,.\{2\},0x& ,g' | xargs printf '%d.%d.%d.%d\n'
+else
+ echo "$output"
+fi
diff --git a/bin/qbrowser b/bin/qbrowser
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec qutebrowser --enable-webengine-inspector "$@"+
\ No newline at end of file
diff --git a/bin/quadrigacx b/bin/quadrigacx
@@ -0,0 +1,10 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p jq
+nbtc=33
+
+pair=XXBTZCAD
+cad=$(curl -sL "https://api.kraken.com/0/public/Ticker?pair=$pair" | jq -r ".result.$pair.a[0]")
+#usd=$(curl -sL 'https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD' | jq '.[0][1]' &)
+#cad=$(curl -sL 'https://apiv2.bitcoinaverage.com/indices/global/ticker/all?crypto=BTC&fiat=CAD' | jq -r '.BTCCAD.last')
+calc=$(bc -l <<<"$cad * $nbtc")
+printf "$%s, ($%s)\n" "$cad" "$(nfmt <<<$calc)"
diff --git a/bin/razorcx-mkrepo b/bin/razorcx-mkrepo
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+QUOTE_ARGS=''
+for ARG in "$@"
+do
+ ARG=$(printf "%q" "$ARG")
+ QUOTE_ARGS="${QUOTE_ARGS} $ARG"
+done
+
+exec ssh razorcx "/var/git/mkrepo $QUOTE_ARGS"
diff --git a/bin/reader b/bin/reader
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec urxvtc -fn 'xft:Inconsolata:size=18' -e "$@"
diff --git a/bin/remind b/bin/remind
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf "usage: remind 'reminder' 14:25\n" 1>&2
+ exit 1
+}
+
+[ -z "$2" ] || [ -z "$1" ] && usage
+
+echo "notify-send -u critical \"$1\"" | at "$2"
diff --git a/bin/repair-utf8 b/bin/repair-utf8
@@ -0,0 +1,22 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Encode qw( decode FB_QUIET );
+
+binmode STDIN, ':bytes';
+binmode STDOUT, ':encoding(UTF-8)';
+
+my $out;
+
+while ( <> ) {
+ $out = '';
+ while ( length ) {
+ # consume input string up to the first UTF-8 decode error
+ $out .= decode( "utf-8", $_, FB_QUIET );
+ # consume one character; all octets are valid Latin-1
+ $out .= decode( "iso-8859-1", substr( $_, 0, 1 ), FB_QUIET ) if length;
+ }
+ print $out;
+}+
\ No newline at end of file
diff --git a/bin/rgp b/bin/rgp
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec rg -p "$@" | less -R
diff --git a/bin/rss b/bin/rss
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+ssh charon r2e $@
diff --git a/bin/rss-add b/bin/rss-add
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+ssh charon "r2e add $1 '$2'"
diff --git a/bin/rss-del b/bin/rss-del
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+ssh charon "r2e del $1"+
\ No newline at end of file
diff --git a/bin/runlog b/bin/runlog
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+systemctl restart "$@"; journalctl -fu "$@"
diff --git a/bin/runlogu b/bin/runlogu
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+sysctlu restart "$1" && jctlu -fu "$1"
diff --git a/bin/rust-dev b/bin/rust-dev
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+export RUST_CHANNEL=${RUST_CHANNEL:-stable}
+export RUST_SRC_PATH="$(nix-build '<nixpkgs>' --no-out-link -A rustChannels.$RUST_CHANNEL.rust-src)"/lib/rustlib/src/rust/src
+export LD_LIBRARY_PATH="$(nix-build '<nixpkgs>' --no-out-link -A rustChannels.$RUST_CHANNEL.rustc)"/lib:$LD_LIBRARY_PATH
+# texlive.combined.scheme-full \
+
+#export LIBCLANG_PATH="$(nix-build '<nixpkgs>' --no-out-link -A llvmPackages.libclang)/lib"
+
+# cargo-bloat \
+
+# secp256k1 \
+ #librsvg \
+
+exec nix-shell -p \
+ rustChannels.$RUST_CHANNEL.clippy-preview \
+ rustChannels.$RUST_CHANNEL.rust \
+ rustracer \
+ "$@"
diff --git a/bin/sedcut b/bin/sedcut
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec sed -n 's,.*'"$1"'.*,\1,p'
diff --git a/bin/sedit b/bin/sedit
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+[[ -z $1 ]] && exit 1
+path="$(spath $1)"
+if [[ ! -e "$path" ]]; then
+ printf "$1 not found\n" 1>&2
+ exit 2
+fi
+shift
+$EDITOR "$@" "$path"
+
diff --git a/bin/sendmail b/bin/sendmail
@@ -0,0 +1,11 @@
+#! /usr/bin/env sh
+
+export EMAIL_CUSTOM_CONN_TEST=/home/jb55/bin/email-conn-test
+export EMAIL_CONN_TEST=c
+
+# pass all params to msmtpq & redirect output
+msmtpq "$@" >> /tmp/msmtp.log 2> /tmp/msmtp.err
+
+# always succeed, even on connection/mail failure
+# we'll simply queue the mail in our outbox
+exit 0
diff --git a/bin/sendmailq b/bin/sendmailq
@@ -0,0 +1,2 @@
+#!/bin/sh
+msmtpq --read-envelope-from -C /home/jb55/.msmtprc -t
diff --git a/bin/skel b/bin/skel
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec cp -vr --no-clobber $HOME/dotfiles/skeletons/$1/. .
diff --git a/bin/sortur b/bin/sortur
@@ -0,0 +1,2 @@
+#!/bin/sh
+sort | uniq -c | sort -nr "$@" | sed 's,^[[:space:]]*,,g;s,\(^[[:digit:]]\+\) ,\1\t,g'
diff --git a/bin/spath b/bin/spath
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+[[ -z $1 ]] && exit 1
+prog=$(which $1 2>/dev/null)
+[[ $? -ne 0 ]] && exit 2
+line="$(<$prog grep -- $1-wrapped)"
+
+if [[ ! -z "$line" ]]; then
+ <<<"$line" cut -d" " -f4 | sed 's/^"//;s/"$//'
+else
+ echo $prog
+fi
diff --git a/bin/spotify-next b/bin/spotify-next
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+dbus-send --print-reply --dest="$(spotify-service)" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next
diff --git a/bin/spotify-open b/bin/spotify-open
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+dbus-send --print-reply --dest="$(spotify-service)" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.OpenUri string:"$1"
diff --git a/bin/spotify-playpause b/bin/spotify-playpause
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+dbus-send --print-reply --dest="$(spotify-service)" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause
diff --git a/bin/spotify-prev b/bin/spotify-prev
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotifyd /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous
diff --git a/bin/spotify-service b/bin/spotify-service
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+options=$(dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames \
+ | grep spotify \
+ | sedcut '"\([^"]\+\)"')
+
+grep 'spotify$' <<<"$options" || grep -v 'spotify$' <<<"$options"
diff --git a/bin/spotify-stop b/bin/spotify-stop
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Stop+
\ No newline at end of file
diff --git a/bin/sql-in-strings b/bin/sql-in-strings
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+xargs printf "\\'%s\\'\n" | paste -sd, | xargs printf "(%s)\n"
diff --git a/bin/start-hoogle b/bin/start-hoogle
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+hoogle server --local -p 8080 &> /tmp/hoogle.log
diff --git a/bin/stealthium b/bin/stealthium
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+vpn chromium --profile-directory=stealthium
diff --git a/bin/stripansi b/bin/stripansi
@@ -0,0 +1 @@
+perl -pe 's/\e\[?.*?[\@-~]//g'
diff --git a/bin/superclean b/bin/superclean
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+b="0"
+for p in `ghc-pkg check $* 2>&1 | grep problems | awk '{print $6}' | sed -e 's/:$//'`
+do
+ echo unregistering $p; ghc-pkg $* unregister $p;
+ cabal -j12 install $p; b="1"
+done
diff --git a/bin/svg2png b/bin/svg2png
@@ -0,0 +1,12 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p inkscape
+
+usage () {
+ printf "usage: svg2png file.svg <width> [height]\n"
+ exit 1
+}
+
+png="${1%.svg}.png"
+[ -z "$1" ] || [ -z "$2" ] && usage
+height=${3:-$2}
+inkscape -z -e "$png" -w "$2" -h "$height" "$1"
diff --git a/bin/switch-term-themes b/bin/switch-term-themes
@@ -0,0 +1,13 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i python -p python
+
+import os
+import sys
+import subprocess
+
+theme = sys.argv[1]
+pts = os.listdir('/dev/pts/')
+for each_pts in pts:
+ if each_pts.isdigit():
+ subprocess.call('echo "`~/.dynamic-colors/bin/dynamic-colors switch {0}`" > /dev/pts/{1}'.format(theme,each_pts), shell=True)
+
diff --git a/bin/switch_ghc b/bin/switch_ghc
@@ -0,0 +1,2 @@
+#!/bin/sh
+sudo $HOME/bin/brent switch -s /usr/local/stow $@
diff --git a/bin/sync-todo b/bin/sync-todo
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+
+from subprocess import call, PIPE
+import os
+import socket
+import sys
+
+remotes = {
+ 'quiver': "172.24.129.211",
+ 'monad': "172.24.242.111"
+};
+
+DEVNULL = open(os.devnull, 'w')
+
+def process(frm, to, file):
+ hostname = socket.gethostname()
+ dest = remotes.get(to)
+ src = remotes.get(frm)
+
+ if dest is None or src is None:
+ return 1
+
+ if frm == hostname:
+ cmd = "scp {} {}:{}".format(file, dest, file).split(" ")
+ else:
+ cmd = "scp {}:{} {}".format(src, file, file).split(" ")
+ return call(cmd, stdout=DEVNULL, stderr=DEVNULL)
+
+argc = len(sys.argv)
+
+if argc != 3:
+ exit(1)
+
+frm = sys.argv[1]
+to = sys.argv[2]
+file = "/home/jb55/projects/razorcx/doc/org/todo.org"
+
+[_,bn] = os.path.split(file)
+
+ret = process(frm, to, file)
+
+if ret != 0:
+ print("failed sync {}, {} -> {}".format(bn, frm, to))
+else:
+ print("sync success {}, {} -> {}".format(bn, frm, to))
+
+exit(ret)
diff --git a/bin/sync-work b/bin/sync-work
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+rsync -avz --no-perms --no-owner --no-group archer:Dropbox/doc/monstercat/ $HOME/Dropbox/doc/monstercat/
diff --git a/bin/sync_music b/bin/sync_music
@@ -0,0 +1,2 @@
+#!/bin/bash
+rsync -ravP --delete --modify-window=1 ~/Music/iTunes/iTunes\ Media/Music/ /Volumes/NO\ NAME/Music/
diff --git a/bin/sysctlu b/bin/sysctlu
@@ -0,0 +1,2 @@
+#!/bin/sh
+systemctl --user "$@"
diff --git a/bin/termcolor b/bin/termcolor
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+DIR=${XTHEMEDIR:-"$HOME/.Xresources.d/themes"}
+
+[ -z "$1" ] && printf "usage: termcolor <dark|light>\n" && exit 1
+
+rm -f "$DIR/current"
+ln -s "$DIR/$1" "$DIR/current"
+
+xrdb -load "$HOME/.Xresources"
+
+[ "$2" == "kill" ] && pkill urxvt
diff --git a/bin/test-signal b/bin/test-signal
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+import dbus
+import datetime
+import gobject
+import os
+from dbus.mainloop.glib import DBusGMainLoop
+
+def start_work():
+ print("starting work notifier")
+ os.system("systemctl stop --user home-email-notifier")
+ os.system("systemctl start --user work-email-notifier")
+
+def start_home():
+ print("starting home notifier")
+ os.system("systemctl stop --user work-email-notifier")
+ os.system("systemctl start --user home-email-notifier")
+
+def check():
+ now = datetime.datetime.now()
+ if now.isoweekday() > 5:
+ start_home()
+ else:
+ if now.hour > 17 or now.hour < 9:
+ start_home()
+ else:
+ start_work()
+
+def handle_sleep_callback(sleeping):
+ if not sleeping:
+ # awoke from sleep
+ check()
+
+DBusGMainLoop(set_as_default=True) # integrate into main loob
+bus = dbus.SystemBus() # connect to dbus system wide
+bus.add_signal_receiver( # defince the signal to listen to
+ handle_sleep_callback, # name of callback function
+ 'PrepareForSleep', # signal name
+ 'org.freedesktop.login1.Manager', # interface
+ 'org.freedesktop.login1' # bus name
+)
+
+loop = gobject.MainLoop() # define mainloop
+loop.run()
diff --git a/bin/themeswitch b/bin/themeswitch
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf "usage: themeswitch <dark|light>\n"
+ exit 1
+}
+
+theme="$1"
+
+[ -z "$theme" ] && usage
+[ "$theme" != "dark" ] && [ "$theme" != "light" ] && usage
+
+termcolor "$theme" &
+pkill --signal SIGUSR1 xmonad
+switch-term-themes "$theme" &
+
+if [ "$theme" == "light" ]; then
+ feh --bg-fill ~/var/img/wallpapers/wireframe-deer-white.jpg &
+ exec emacsclient -s $HOME/.emacs.d/server/server --eval "(jb55/themeswitch 'light)" &
+else
+ feh --bg-fill ~/var/img/wallpapers/red-low-poly.png &
+ exec emacsclient -s $HOME/.emacs.d/server/server --eval "(jb55/themeswitch 'dark)" &
+fi
+
+wait
diff --git a/bin/tx b/bin/tx
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+bcli getrawtransaction "$1" 2+
\ No newline at end of file
diff --git a/bin/ud b/bin/ud
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf 'Usage: ud <term> [# results]\n'
+ printf '\nurban dictionary search\n\n'
+ exit 1
+}
+
+if [ -z "$1" ]; then
+ usage
+ exit 1
+fi
+
+term="$1"
+n=${2:-1}
+
+BOLD='\u001b[1m'
+RESET='\u001b[0m'
+
+if [[ $n -gt 1 ]]; then
+ format='---------------------------\n'
+else
+ format=""
+fi
+
+format+=$(cat <<EOS
+${BOLD}Definition${RESET}
+
+ \\(.definition | sub("\\n";"\\n "))
+
+${BOLD}Example${RESET}
+
+ \\(.example | sub("\\n";"\\n "))
+EOS
+)
+
+curl -GsL 'https://api.urbandictionary.com/v0/define' --data-urlencode "term=$term" | \
+ jq -C -r 'limit('"$n"';.list[]) | "'"$format"'"'
diff --git a/bin/unzip-stream b/bin/unzip-stream
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+import zipfile
+import sys
+import StringIO
+data = StringIO.StringIO(sys.stdin.read())
+z = zipfile.ZipFile(data)
+dest = sys.argv[1] if len(sys.argv) == 2 else '.'
+z.extractall(dest)
diff --git a/bin/utxo b/bin/utxo
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+bcli scantxoutset start '["'"$1"'"]'
diff --git a/bin/vbox-guest-ip b/bin/vbox-guest-ip
@@ -0,0 +1,3 @@
+#!/bin/sh
+# requires: sedcut, virtualbox
+VBoxManage guestproperty enumerate "$1" | grep -Ee 'Net.*IP' | sedcut 'value: \([0-9\.]\+\)'
diff --git a/bin/vipe b/bin/vipe
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+
+#
+# vipe(1) - Pipe in and out of $EDITOR
+#
+# (c) 2014 Julian Gruber <julian@juliangruber.com>.
+# MIT licensed.
+#
+# Example:
+#
+# $ echo foo | vipe | gist
+# $ vipe | gist
+#
+# This is a lightweight bash only version. For the original impementation in
+# python, check https://github.com/madx/moreutils/blob/master/vipe
+#
+
+# version
+
+VERSION="0.1.0"
+
+# usage
+
+if [ ${1} ]; then
+ case "${1}" in
+ "-h")
+ echo "usage: vipe [-hV]"
+ exit 0 ;;
+ "-V")
+ echo "$VERSION"
+ exit 0 ;;
+ *)
+ echo "unknown option: \"${1}\""
+ echo "usage: vipe [-hV]"
+ exit 1
+ esac
+fi
+
+# temp file
+
+t=/tmp/vipe.$$.txt
+touch $t
+
+# read from stdin
+
+if [ ! -t 0 ]; then
+ cat > $t
+fi
+
+# spawn editor with stdio connected
+
+${EDITOR} $t < /dev/tty > /dev/tty || exit $?
+
+# write to stdout
+
+cat $t
+
+# cleanup
+
+rm $t
+
diff --git a/bin/vmclose b/bin/vmclose
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+VM=${1:-razoredge}
+
+# pause vms
+if type VBoxManage > /dev/null; then
+ if VBoxManage controlvm "$VM" savestate
+ then
+ notify-send "$VM saved"
+ else
+ notify-send "$VM already saved"
+ fi
+fi
+
+sleep 5
+pkill VirtualBox
diff --git a/bin/vmtoggle b/bin/vmtoggle
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+VM=razoredge
+
+control () {
+ VBoxManage controlvm $VM "$1" && notify-send "$VM ${1}d"
+}
+
+control pause || control resume
+
diff --git a/bin/vpn b/bin/vpn
@@ -0,0 +1,4 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p libcgroup
+
+cgexec --sticky -g net_cls:pia $@
diff --git a/bin/vpnrun b/bin/vpnrun
@@ -0,0 +1,3 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p libcgroup
+cgexec --sticky -g net_cls:pia "$@"
diff --git a/bin/walltime b/bin/walltime
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+exec ps -p "$(pgrep "$1")" -o etime
diff --git a/bin/weather b/bin/weather
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec curl wttr.in/vancouver
diff --git a/bin/wifie b/bin/wifie
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec sudoedit /etc/wpa_supplicant.conf
diff --git a/bin/wifir b/bin/wifir
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec runlog wpa_supplicant
diff --git a/bin/wifis b/bin/wifis
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+script=$(cat <<'EOF'
+s,--,\n,g;
+s,\t[ ]\+,\t,g;
+s,ESSID:,,g;
+s,",,g;
+s,Encryption key:\([^\t]\+\),\1,g;
+s,Bit Rates:,,g;
+s,^[ ]\+,,g;
+s,;,\t,g
+EOF
+)
+
+sudo iwlist wlp4s0 scan \
+ | grep -A6 -i -e key \
+ | tr '\n' $'\t' \
+ | sed "$script" \
+ | sort -u \
+ | columnt "$@" \
+ | less -R -S
+
+
+
diff --git a/bin/xdg-open b/bin/xdg-open
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+
+readonly CONFIG="$HOME/.config/mimi/mime.conf"
+
+find_in_config() {
+ [[ -f "$CONFIG" ]] || return
+ grep -m 1 "^$1: " "$CONFIG" | cut -d ' ' -f 2-
+}
+
+need_terminal() {
+ grep -m 1 -q '^Terminal=true$'
+}
+
+find_exec_in_desktop_file() {
+ awk -F '[= ]' '$1 == "Exec" {print $2; exit}'
+}
+
+find_desktop_file_by() {
+ [[ -d "$HOME/.local/share/applications" ]] && path+=("$HOME/.local/share/applications")
+ grep -m 1 "^$1=.*$2" -R "${path[@]}" | awk -F : -v pat="$2" '{ print index($2, pat), length($2), $1 }' | sort -t ' ' -k1,1n -k2,2nr | awk '{ print $3; exit }'
+}
+
+url_decode() {
+ echo -e "$(sed 's/%\([a-f0-9A-F]\{2\}\)/\\x\1/g')"
+}
+
+fork_run() {
+ echo "$*"
+ "$@" &>/dev/null &
+ exit 0
+}
+
+exist() {
+ type "$@" &>/dev/null
+}
+
+usage() {
+ cat <<-EOF
+
+ Usage: xdg-open [file|directory|protocol]
+
+ It opens a file according to the extension
+ To setup the extension, create $CONFIG
+
+ mimi :)
+ EOF
+ exit 1
+}
+
+# config
+# 1. ext
+# 2. protocol
+# 3. mime
+# 4. general mime
+# .desktop (mime and general mime)
+# 5. ask
+
+[[ ! "$*" ]] && usage
+
+arg="$*"
+ext=''
+protocol=''
+mime=''
+general_mime=''
+
+# fix file://
+if [[ "$arg" =~ ^file://(.*)$ ]]; then
+ # strip file://
+ arg="$(url_decode <<<"${BASH_REMATCH[1]}")"
+ protocol=file
+fi
+
+if [[ -e "$arg" ]]; then
+ # file or dir
+ mime="$(file -ib "$arg" | cut -d';' -f1)"
+ if [[ -f "$arg" ]]; then
+ ext="$(tr '[:upper:]' '[:lower:]' <<< "${arg##*.}")"
+ fi
+fi
+
+# protocol to mime ext
+if [[ "$arg" =~ ^([a-zA-Z-]+): ]]; then
+ # use protocol to guess mime ext
+ protocol="${BASH_REMATCH[1]}"
+ case "$protocol" in
+ http|https)
+ mime=text/html
+ ext=html
+ ;;
+ magnet)
+ mime=application/x-bittorrent
+ ext=torrent
+ ;;
+ irc)
+ mime=x-scheme-handler/irc
+ ;;
+ esac
+fi
+
+
+# application mime is specific
+[[ "$mime" =~ ^(audio|image|text|video)/ ]] && general_mime="${BASH_REMATCH[1]}/"
+
+exist "$TERM" || TERM="$(find_in_config TERM)"
+
+# config
+for search in $ext $protocol $mime $general_mime; do
+ app=($(find_in_config "$search"))
+ [[ "${app[0]}" == TERM ]] && exist "$TERM" && app[0]="$TERM"
+ [[ "${app[*]}" ]] && fork_run "${app[@]}" "$arg"
+done
+
+# .desktop
+for search in $mime $general_mime; do
+ desktop="$(find_desktop_file_by MimeType "$search")"
+ if [[ "$desktop" ]]; then
+ echo "$desktop"
+ app=($(find_exec_in_desktop_file <"$desktop"))
+ if need_terminal <"$desktop"; then
+ echo "term: $TERM"
+ exist "$TERM" && fork_run "$TERM" -e "${app[@]}" "$arg"
+ else
+ fork_run "${app[@]}" "$arg"
+ fi
+ fi
+done
+
+# ask
+if exist dmenu; then
+ app=($(IFS=: stest -flx $PATH | sort -u | dmenu -p "how to open $arg"))
+ [[ "${app[*]}" ]] && fork_run "${app[@]}" "$arg"
+fi
diff --git a/bin/xml2sexp b/bin/xml2sexp
@@ -0,0 +1,3 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p libxslt bash
+xsltproc $HOME/bin/xml2sexp.xsl ${1:-/dev/stdin} | prettysexp
diff --git a/bin/xml2sexp.xsl b/bin/xml2sexp.xsl
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--$Id: xml2sexp.xsl 4047 2006-08-11 19:11:17Z tv.raman.tv $-->
+<!--
+Author: T. V. Raman
+Copyright:GPL
+Description:Convert XML to a Lisp S-expression.
+Goal: Replace Emacs' xml-parse.el with equivalent functionality
+Shortcomings: Quotes in PCDATA will be lost
+Still very slow, possibly write a native libxml2 app?
+-->
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ version="1.0">
+
+ <xsl:output method="text"/>
+ <xsl:template match="text()">
+ <xsl:variable name="text" select="normalize-space()"/>
+ <xsl:if test="$text">
+ "<xsl:value-of select="$text"/>"
+ </xsl:if>
+ </xsl:template>
+
+ <xsl:template match="*">
+ (<xsl:choose><xsl:when test="@*">(<xsl:value-of select="name()"/><xsl:apply-templates select="@*"/>)</xsl:when>
+ <xsl:otherwise><xsl:value-of select="name()"/></xsl:otherwise></xsl:choose><xsl:apply-templates/>)
+ </xsl:template>
+
+ <xsl:template match="@*">
+ (<xsl:value-of select="name()"/> . "<xsl:value-of select="."/>")
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/bin/xmlfmt b/bin/xmlfmt
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec xmllint --format "$@"
diff --git a/bin/xxdrp b/bin/xxdrp
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec xxd -r -p "$@"
diff --git a/bin/z.sh b/bin/z.sh
@@ -0,0 +1,245 @@
+# Copyright (c) 2009 rupa deadwyler under the WTFPL license
+
+# maintains a jump-list of the directories you actually use
+#
+# INSTALL:
+# * put something like this in your .bashrc/.zshrc:
+# . /path/to/z.sh
+# * cd around for a while to build up the db
+# * PROFIT!!
+# * optionally:
+# set $_Z_CMD in .bashrc/.zshrc to change the command (default z).
+# set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z).
+# set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
+# set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself.
+# set $_Z_EXCLUDE_DIRS to an array of directories to exclude.
+# set $_Z_OWNER to your username if you want use z while sudo with $HOME kept
+#
+# USE:
+# * z foo # cd to most frecent dir matching foo
+# * z foo bar # cd to most frecent dir matching foo and bar
+# * z -r foo # cd to highest ranked dir matching foo
+# * z -t foo # cd to most recently accessed dir matching foo
+# * z -l foo # list matches instead of cd
+# * z -c foo # restrict matches to subdirs of $PWD
+
+[ -d "${_Z_DATA:-$HOME/.z}" ] && {
+ echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
+}
+
+_z() {
+
+ local datafile="${_Z_DATA:-$HOME/.z}"
+
+ # bail if we don't own ~/.z and $_Z_OWNER not set
+ [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return
+
+ # add entries
+ if [ "$1" = "--add" ]; then
+ shift
+
+ # $HOME isn't worth matching
+ [ "$*" = "$HOME" ] && return
+
+ # don't track excluded directory trees
+ local exclude
+ for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
+ case "$*" in "$exclude*") return;; esac
+ done
+
+ # maintain the data file
+ local tempfile="$datafile.$RANDOM"
+ while read line; do
+ # only count directories
+ [ -d "${line%%\|*}" ] && echo $line
+ done < "$datafile" | awk -v path="$*" -v now="$(date +%s)" -F"|" '
+ BEGIN {
+ rank[path] = 1
+ time[path] = now
+ }
+ $2 >= 1 {
+ # drop ranks below 1
+ if( $1 == path ) {
+ rank[$1] = $2 + 1
+ time[$1] = now
+ } else {
+ rank[$1] = $2
+ time[$1] = $3
+ }
+ count += $2
+ }
+ END {
+ if( count > 9000 ) {
+ # aging
+ for( x in rank ) print x "|" 0.99*rank[x] "|" time[x]
+ } else for( x in rank ) print x "|" rank[x] "|" time[x]
+ }
+ ' 2>/dev/null >| "$tempfile"
+ # do our best to avoid clobbering the datafile in a race condition
+ if [ $? -ne 0 -a -f "$datafile" ]; then
+ env rm -f "$tempfile"
+ else
+ [ "$_Z_OWNER" ] && chown $_Z_OWNER:$(id -ng $_Z_OWNER) "$tempfile"
+ env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile"
+ fi
+
+ # tab completion
+ elif [ "$1" = "--complete" -a -s "$datafile" ]; then
+ while read line; do
+ [ -d "${line%%\|*}" ] && echo $line
+ done < "$datafile" | awk -v q="$2" -F"|" '
+ BEGIN {
+ if( q == tolower(q) ) imatch = 1
+ q = substr(q, 3)
+ gsub(" ", ".*", q)
+ }
+ {
+ if( imatch ) {
+ if( tolower($1) ~ tolower(q) ) print $1
+ } else if( $1 ~ q ) print $1
+ }
+ ' 2>/dev/null
+
+ else
+ # list/go
+ while [ "$1" ]; do case "$1" in
+ --) while [ "$1" ]; do shift; local fnd="$fnd${fnd:+ }$1";done;;
+ -*) local opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
+ c) local fnd="^$PWD $fnd";;
+ e) local echo=echo;;
+ h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
+ l) local list=1;;
+ r) local typ="rank";;
+ t) local typ="recent";;
+ x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
+ esac; opt=${opt:1}; done;;
+ *) local fnd="$fnd${fnd:+ }$1";;
+ esac; local last=$1; [ "$#" -gt 0 ] && shift; done
+ [ "$fnd" -a "$fnd" != "^$PWD " ] || local list=1
+
+ # if we hit enter on a completion just go there
+ case "$last" in
+ # completions will always start with /
+ /*) [ -z "$list" -a -d "$last" ] && cd "$last" && return;;
+ esac
+
+ # no file yet
+ [ -f "$datafile" ] || return
+
+ local cd
+ cd="$(while read line; do
+ [ -d "${line%%\|*}" ] && echo $line
+ done < "$datafile" | awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
+ function frecent(rank, time) {
+ # relate frequency and time
+ dx = t - time
+ if( dx < 3600 ) return rank * 4
+ if( dx < 86400 ) return rank * 2
+ if( dx < 604800 ) return rank / 2
+ return rank / 4
+ }
+ function output(files, out, common) {
+ # list or return the desired directory
+ if( list ) {
+ cmd = "sort -n >&2"
+ for( x in files ) {
+ if( files[x] ) printf "%-10s %s\n", files[x], x | cmd
+ }
+ if( common ) {
+ printf "%-10s %s\n", "common:", common > "/dev/stderr"
+ }
+ } else {
+ if( common ) out = common
+ print out
+ }
+ }
+ function common(matches) {
+ # find the common root of a list of matches, if it exists
+ for( x in matches ) {
+ if( matches[x] && (!short || length(x) < length(short)) ) {
+ short = x
+ }
+ }
+ if( short == "/" ) return
+ # use a copy to escape special characters, as we want to return
+ # the original. yeah, this escaping is awful.
+ clean_short = short
+ gsub(/\[\(\)\[\]\|\]/, "\\\\&", clean_short)
+ for( x in matches ) if( matches[x] && x !~ clean_short ) return
+ return short
+ }
+ BEGIN {
+ gsub(" ", ".*", q)
+ hi_rank = ihi_rank = -9999999999
+ }
+ {
+ if( typ == "rank" ) {
+ rank = $2
+ } else if( typ == "recent" ) {
+ rank = $3 - t
+ } else rank = frecent($2, $3)
+ if( $1 ~ q ) {
+ matches[$1] = rank
+ } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank
+ if( matches[$1] && matches[$1] > hi_rank ) {
+ best_match = $1
+ hi_rank = matches[$1]
+ } else if( imatches[$1] && imatches[$1] > ihi_rank ) {
+ ibest_match = $1
+ ihi_rank = imatches[$1]
+ }
+ }
+ END {
+ # prefer case sensitive
+ if( best_match ) {
+ output(matches, best_match, common(matches))
+ } else if( ibest_match ) {
+ output(imatches, ibest_match, common(imatches))
+ }
+ }
+ ')"
+ [ $? -gt 0 ] && return
+ [ "$cd" ] || return
+ ${echo:-cd} "$cd"
+ fi
+}
+
+alias ${_Z_CMD:-z}='_z 2>&1'
+
+[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
+
+if type compctl >/dev/null 2>&1; then
+ # zsh
+ [ "$_Z_NO_PROMPT_COMMAND" ] || {
+ # populate directory list, avoid clobbering any other precmds.
+ if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
+ _z_precmd() {
+ _z --add "${PWD:a}"
+ }
+ else
+ _z_precmd() {
+ _z --add "${PWD:A}"
+ }
+ fi
+ [[ -n "${precmd_functions[(r)_z_precmd]}" ]] || {
+ precmd_functions[$(($#precmd_functions+1))]=_z_precmd
+ }
+ }
+ _z_zsh_tab_completion() {
+ # tab completion
+ local compl
+ read -l compl
+ reply=(${(f)"$(_z --complete "$compl")"})
+ }
+ compctl -U -K _z_zsh_tab_completion _z
+elif type complete >/dev/null 2>&1; then
+ # bash
+ # tab completion
+ complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
+ [ "$_Z_NO_PROMPT_COMMAND" ] || {
+ # populate directory list. avoid clobbering other PROMPT_COMMANDs.
+ grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || {
+ PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null;'
+ }
+ }
+fi
diff --git a/bin/zoom b/bin/zoom
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf 'usage: zoom https://zoom.us/j/123456\n'
+ exit 1
+}
+
+if [ -z "$1" ]; then
+ usage
+fi
+
+exec open $(zoom-uri "$1")
diff --git a/bin/zoom-id b/bin/zoom-id
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+exec open $(zoom-id-uri "$@")
diff --git a/bin/zoom-id-uri b/bin/zoom-id-uri
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+printf 'zoommtg://zoom.us/?action=join&confno=%d\n' "$1"+
\ No newline at end of file
diff --git a/bin/zoom-uri b/bin/zoom-uri
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+usage () {
+ printf 'usage: zoom-uri https://zoom.us/j/123456\n'
+ exit 1
+}
+
+if [ -z "$1" ]; then
+ usage
+fi
+
+exec sed -E 's,https://([a-zA-Z]+\.)?zoom.us/j/([[:digit:]]+),zoommtg://zoom.us/?action=join\&confno=\2,' <<<"$1"