citadel

My dotfiles, scripts and nix configs
git clone git://jb55.com/citadel
Log | Files | Refs | README | LICENSE

commit 11e4ada074a4971339deb90a0b358879ab777c9b
parent 0babad5344527de0c554bcfb2e8266e45c3ca5cd
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 15 Sep 2020 20:57:58 -0700

add bin tree

Diffstat:
Abin/.gitignore | 14++++++++++++++
Abin/.gitmodules | 6++++++
Abin/all-dev | 7+++++++
Abin/ariline | 1+
Abin/av98 | 1585+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/awkp | 5+++++
Abin/awkt | 2++
Abin/baby-mic | 6++++++
Abin/bats | 5+++++
Abin/bats-job | 10++++++++++
Abin/bc-exp | 2++
Abin/bcli | 2++
Abin/bip | 2++
Abin/bittorrent | 3+++
Abin/bright | 2++
Abin/browser | 15+++++++++++++++
Abin/btc | 6++++++
Abin/btc-balance | 7+++++++
Abin/btc-blockfees | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/btc-blocks | 2++
Abin/btc-blocktimes | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/btc-blocktimes-pretty | 2++
Abin/btc-coins | 10++++++++++
Abin/btc-halvening | 11+++++++++++
Abin/btc-lastblock | 2++
Abin/btc-next-difficulty | 24++++++++++++++++++++++++
Abin/btc-price | 8++++++++
Abin/btc-returns | 26++++++++++++++++++++++++++
Abin/btc-tx-cost | 23+++++++++++++++++++++++
Abin/btc-txs | 2++
Abin/btc-whoa | 13+++++++++++++
Abin/capcom | 2++
Abin/cert | 14++++++++++++++
Abin/chat | 47+++++++++++++++++++++++++++++++++++++++++++++++
Abin/chrome | 10++++++++++
Abin/chromecast | 4++++
Abin/chromecast-stop | 4++++
Abin/clearmime | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/cmd | 3+++
Abin/colorpick | 6++++++
Abin/columnt | 2++
Abin/commands | 4++++
Abin/connect-bose | 3+++
Abin/coretto-emails | 6++++++
Abin/cs | 2++
Abin/ct | 2++
Abin/curlsl | 2++
Abin/cutt | 2++
Abin/dclip | 2++
Abin/devpeople | 23+++++++++++++++++++++++
Abin/dmenu | 2++
Abin/dmenu-lpass | 31+++++++++++++++++++++++++++++++
Abin/dmenup | 15+++++++++++++++
Abin/dmenupn | 5+++++
Abin/dog | 3+++
Abin/ds4-connect | 10++++++++++
Abin/ds4-disconnect | 5+++++
Abin/ds4_battery | 9+++++++++
Abin/dswitcher | 27+++++++++++++++++++++++++++
Abin/dupfiles | 2++
Abin/dynamic-linker | 2++
Abin/edit | 4++++
Abin/edit-clipboard | 2++
Abin/emacs-dev | 2++
Abin/emacs-mailto-handler | 31+++++++++++++++++++++++++++++++
Abin/emacsc | 2++
Abin/email-conn-test | 2++
Abin/email-fetch | 2++
Abin/email-status | 7+++++++
Abin/email-status-once | 14++++++++++++++
Abin/fetch-work-mail | 3+++
Abin/find-dir | 4++++
Abin/find-file | 4++++
Abin/focus-wow | 3+++
Abin/focus-zoom | 5+++++
Abin/fsize | 2++
Abin/gaps | 2++
Abin/gemini | 2++
Abin/gh | 0
Abin/gh-add-refs | 9+++++++++
Abin/gh-clone | 46++++++++++++++++++++++++++++++++++++++++++++++
Abin/ghcWithPackages | 2++
Abin/ghissue | 2++
Abin/gifenc | 8++++++++
Abin/git-author-stats | 31+++++++++++++++++++++++++++++++
Abin/git-bvr | 2++
Abin/git-checkout-pr | 5+++++
Abin/git-checkout-remote | 22++++++++++++++++++++++
Abin/git-cherry-pick-squashed | 0
Abin/git-contacts | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/git-find-blob | 48++++++++++++++++++++++++++++++++++++++++++++++++
Abin/git-fixup | 4++++
Abin/git-format-patches | 0
Abin/git-gh-merge | 17+++++++++++++++++
Abin/git-github-refs | 8++++++++
Abin/git-logbr | 4++++
Abin/git-logm | 6++++++
Abin/git-logr | 6++++++
Abin/git-pr | 11+++++++++++
Abin/git-pr-event | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/git-review | 33+++++++++++++++++++++++++++++++++
Abin/git-sha256 | 4++++
Abin/google-group-subscribe | 8++++++++
Abin/gpginfo | 1+
Abin/grabssh | 8++++++++
Abin/group_permissions | 13+++++++++++++
Abin/hackage-docs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/haskell-emacs | 2++
Abin/haskell-shell | 2++
Abin/headers | 3+++
Abin/hex2dec | 9+++++++++
Abin/hist | 13+++++++++++++
Abin/hsp | 12++++++++++++
Abin/htmlinit | 28++++++++++++++++++++++++++++
Abin/initx | 9+++++++++
Abin/jctlu | 2++
Abin/killchrome | 2++
Abin/killsession | 2++
Abin/lcli | 2++
Abin/lclitn | 2++
Abin/lessp | 25+++++++++++++++++++++++++
Abin/lightning-dev | 6++++++
Abin/lm | 2++
Abin/ln-channelinfo | 2++
Abin/ln-forwards | 2++
Abin/ln-nodeinfo | 2++
Abin/load-dark-env | 1+
Abin/load-light-env | 1+
Abin/load-theme-env | 3+++
Abin/lock | 2++
Abin/magnetize | 2++
Abin/makex | 2++
Abin/mdr | 16++++++++++++++++
Abin/mempool-size | 2++
Abin/mount-hdd1 | 3+++
Abin/my-reboot | 6++++++
Abin/my-suspend | 12++++++++++++
Abin/mydate | 2++
Abin/myip | 2++
Abin/n | 4++++
Abin/nannypay | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/netbps | 23+++++++++++++++++++++++
Abin/netscan | 8++++++++
Abin/netscan.sh | 8++++++++
Abin/newlines | 3+++
Abin/nfmt | 2++
Abin/nix-build-cache | 8++++++++
Abin/nix-cabal-build | 17+++++++++++++++++
Abin/nix-cabal-shell | 17+++++++++++++++++
Abin/nix-cp-hash | 2++
Abin/nix-deps | 2++
Abin/nix-eval | 37+++++++++++++++++++++++++++++++++++++
Abin/nix-grep | 9+++++++++
Abin/nix-install | 2++
Abin/nix-lib-path | 3+++
Abin/nix-pkg | 17+++++++++++++++++
Abin/nix-revdep | 2++
Abin/nix-src | 3+++
Abin/nixhash | 2++
Abin/nostat | 38++++++++++++++++++++++++++++++++++++++
Abin/notmuch-personal | 2++
Abin/notmuch-poll | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/notmuch-remote | 3+++
Abin/notmuch-update-mcat | 39+++++++++++++++++++++++++++++++++++++++
Abin/notmuch-update-personal | 2++
Abin/notmuch-work | 2++
Abin/npmrun | 2++
Abin/nsr | 4++++
Abin/open | 2++
Abin/open-dl | 17+++++++++++++++++
Abin/otp | 9+++++++++
Abin/ots-git | 13+++++++++++++
Abin/parallel-chunked | 18++++++++++++++++++
Abin/pdf2remarkable | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/pdfcat | 3+++
Abin/pdfnow | 23+++++++++++++++++++++++
Abin/phlogs | 2++
Abin/phone-batt | 2++
Abin/phone-clipboard | 25+++++++++++++++++++++++++
Abin/phonectl | 6++++++
Abin/postjson | 8++++++++
Abin/prettysexp | 16++++++++++++++++
Abin/proquint-ip | 28++++++++++++++++++++++++++++
Abin/qbrowser | 3+++
Abin/quadrigacx | 10++++++++++
Abin/razorcx-mkrepo | 9+++++++++
Abin/reader | 2++
Abin/remind | 10++++++++++
Abin/repair-utf8 | 23+++++++++++++++++++++++
Abin/rgp | 2++
Abin/rss | 2++
Abin/rss-add | 2++
Abin/rss-del | 3+++
Abin/runlog | 2++
Abin/runlogu | 2++
Abin/rust-dev | 19+++++++++++++++++++
Abin/sedcut | 2++
Abin/sedit | 10++++++++++
Abin/sendmail | 11+++++++++++
Abin/sendmailq | 2++
Abin/skel | 2++
Abin/sortur | 2++
Abin/spath | 11+++++++++++
Abin/spotify-next | 3+++
Abin/spotify-open | 2++
Abin/spotify-playpause | 2++
Abin/spotify-prev | 2++
Abin/spotify-service | 6++++++
Abin/spotify-stop | 3+++
Abin/sql-in-strings | 3+++
Abin/start-hoogle | 2++
Abin/stealthium | 3+++
Abin/stripansi | 1+
Abin/superclean | 8++++++++
Abin/svg2png | 12++++++++++++
Abin/switch-term-themes | 13+++++++++++++
Abin/switch_ghc | 2++
Abin/sync-todo | 47+++++++++++++++++++++++++++++++++++++++++++++++
Abin/sync-work | 2++
Abin/sync_music | 2++
Abin/sysctlu | 2++
Abin/termcolor | 12++++++++++++
Abin/test-signal | 44++++++++++++++++++++++++++++++++++++++++++++
Abin/themeswitch | 25+++++++++++++++++++++++++
Abin/tx | 3+++
Abin/ud | 38++++++++++++++++++++++++++++++++++++++
Abin/unzip-stream | 8++++++++
Abin/utxo | 3+++
Abin/vbox-guest-ip | 3+++
Abin/vipe | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/vmclose | 16++++++++++++++++
Abin/vmtoggle | 10++++++++++
Abin/vpn | 4++++
Abin/vpnrun | 3+++
Abin/walltime | 2++
Abin/weather | 2++
Abin/wifie | 2++
Abin/wifir | 2++
Abin/wifis | 24++++++++++++++++++++++++
Abin/xdg-open | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/xml2sexp | 3+++
Abin/xml2sexp.xsl | 31+++++++++++++++++++++++++++++++
Abin/xmlfmt | 2++
Abin/xxdrp | 2++
Abin/z.sh | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/zoom | 12++++++++++++
Abin/zoom-id | 3+++
Abin/zoom-id-uri | 4++++
Abin/zoom-uri | 12++++++++++++
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"