@ 260802.2

200426.5 · 481b64a667e8d9aca522a0455a00f32dac1f4977

initial commit of ~/bin

This brings over everything that was in my bin-only repository.

I removed scripts that were unused or were no longer understood. I also moved any work-specific scripts to a separate (private) repository.

reading declarations

bin/.gitattributes

new file mode 100644@@ -0,0 +1,3 @@+/remote-old-sync filter=git-crypt-home diff=git-crypt-home+/is-remote-old filter=git-crypt-home diff=git-crypt-home+/ssh-add-keys filter=git-crypt-home diff=git-crypt-home

bin/.gitignore

new file mode 100644@@ -0,0 +1,2 @@+/.git-old+/x86_64

bin/all-list

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++echo "nas+frink+leopard+jaguar+wiggum"

bin/all-pull

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++exec runon-all \+    '(+        cd "$HOME"+        g pull --rebase+        pass git pull --rebase+    )'

bin/arch-install

new file mode 100755@@ -0,0 +1,298 @@+#!/bin/bash++# Recommended steps:+#+#   bootstrap before entering chroot+#   copy this script into INSTALL_DIR+#   rootinit after entering chroot+#   x (if installing graphics)+#   pkgs+#   xpkgs (if installing graphics)+#   userinit+#   kaitinit (optional)+#   grubinit+#+# At this point, log in as the user created above.+#+#   yayinit+#   yaypkgs+#   yayxpkgs (if installing graphics)+#   homeinit+#   xenvinit (if installing graphics)+#   gpginit+#   sshinit+#   scannerinit+#   autologin (for media PCs)+#   nmmedia (for media PCs)++INSTALL_DIR=/mnt++bootstrap() {+  pacstrap "$INSTALL_DIR" base base-devel linux linux-firmware+  genfstab -U "$INSTALL_DIR" >> "$INSTALL_DIR"/etc/fstab+}++rootinit() {+  if [ $# != 1 ]; then+    echo "Usage: $(basename "$0") rootinit <hostname>" >&2+    exit 1+  fi+  hostname="$1"++  ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime+  hwclock --systohc+  sed -i 's/^#\(en_US\.UTF-8 .*\)$/\1/g' /etc/locale.gen+  locale-gen+  echo 'LANG=en_US.UTF-8' > /etc/locale.conf++  echo "$hostname" > /etc/hostname+  {+    echo "127.0.0.1  localhost"+    echo "::1  localhost"+    echo "127.0.1.1  $hostname.localdomain $hostname"+  } >> /etc/hosts++  echo 'COMPRESSXZ=(xz -c -z -0 -)' >> /etc/makepkg.conf++  echo "set root password:"+  passwd+}++x() {+  if [ $# != 1 ]; then+    echo "Usage: $(basename "$0") x <intel | radeon>" >&2+    exit 1+  fi+  driver="$1"++  pacman --noconfirm -S xorg xorg-xinit+  case "$driver" in+    intel)+      pacman --noconfirm -S xf86-video-intel+      ;;+    radeon)+      pacman --noconfirm -S xf86-video-ati+      ;;+    *)+      echo "unrecognized graphics driver" >&2+      exit 1+      ;;+  esac+}++pkgs() {+  pacman --noconfirm -S \+    zsh \+    asp \+    sudo \+    vi \+    alsa-utils \+    aspell aspell-en hunspell hunspell-en_US \+    python python-pip python-setuptools python-virtualenv bpython \+    python2 python2-pip python2-setuptools python2-virtualenv bpython2 \+    flake8 \+    go go-tools \+    rustup \+    grub efibootmgr \+    iputils inetutils whois bind-tools dhcpcd \+    cups \+    shellcheck \+    asciidoc \+    pdftk \+    cronie openssh openssl sshfs sshuttle gnupg tmux nfs-utils enscript \+    docker espeak ffmpeg jq ripgrep youtube-dl fzf \+    iotop htop linux-tools pkgfile pwgen pv strace tig time tree valgrind \+    vim \+    git git-crypt aws-cli s3cmd colordiff curl wget ctags darkhttpd pass rsync+  pkgfile --update+}++xpkgs() {+  pacman --noconfirm -Rsn vim+  pacman --noconfirm -S \+    xterm \+    alacritty \+    k3b konversation konsole dolphin okular \+    qt5 qt5ct oxygen oxygen-icons \+    xsane \+    xbindkeys \+    libnotify \+    kmix \+    dunst \+    i3lock \+    mpv \+    nitrogen \+    numlockx \+    xclip \+    xdotool \+    trayer \+    freerdp \+    gmrun \+    gvim \+    noto-fonts noto-fonts-cjk noto-fonts-emoji noto-fonts-extra \+    firefox+}++userinit() {+  # Use the same group ID on all machines.+  sudo groupmod -n users-old users+  sudo groupadd -g 100 users++  sudo useradd -g users -u 1000 -m -s /bin/zsh \+    -G disk,lp,wheel,log,dbus,network,video,audio,optical,storage,scanner,power \+    andrew+  echo "set andrew password"+  sudo passwd andrew+  echo "make sure '%wheel ALL=(ALL) NOPASSWD: ALL' is in sudoers"+}++kaitinit() {+  sudo useradd -g users -u 1002 -m -s /bin/bash \+    -G disk,lp,wheel,log,dbus,network,video,audio,optical,storage,scanner,power \+    kait+  echo "set kait password"+  sudo passwd kait+  echo "make sure '%wheel ALL=(ALL) NOPASSWD: ALL' is in sudoers"+}++grubinit() {+  grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub+  grub-mkconfig -o /boot/grub/grub.cfg+}++yayinit() {+  cd /tmp || exit 1+  if [ ! -d yay ]; then+    git clone https://aur.archlinux.org/yay+  fi+  cd yay || exit 1+  makepkg -fsi --noconfirm+}++yaypkgs() {+  yay --noconfirm -S \+    neovim-git neovim-plug-git python-neovim-git python2-neovim-git \+    brother-dcp7065dn \+    brscan4 \+    ngrok \+    par \+    vim-fzf-git vim-go-git vim-ledger-git vim-plug-git \+    vim-renamer-git vim-syntastic-git vim-toml-git \+    zsh-autosuggestions \+    zsh-completions \+    zsh-syntax-highlighting \+    zsh-fast-syntax-highlighting-git \+    dict-wn+}++yayxpkgs() {+  yay --noconfirm -S \+    dzen2-xft-xpm-xinerama-git \+    google-chrome \+    mirage \+    idesk \+    gkrellm \+    gkleds \+    gkrellsun \+    gkrellweather \+    nerd-fonts-hack \+    ttf-oxygen \+    ttf-dejavu \+    ttf-fira-mono \+    ttf-mononoki-git \+    ttf-roboto \+    ttf-ubuntu-font-family+}++homeinit() {+  cd "$HOME" || exit 1+  curl https://burntsushi.net/stuff/setup-home > /tmp/setup-home.sh+  sh /tmp/setup-home.sh+  git clone andrew@home.burntsushi.net:/m/git/password-store .password-store+  (cd .password-store && configure-git)++  mkdir -p .gnupg+  {+    echo 'default-cache-ttl 86400'+    echo 'max-cache-ttl 86400'+  } > .gnupg/gpg-agent.conf+}++xenvinit() {+  cd "$HOME" || exit 1++  . "$HOME/bin/x11-go"+  go get -v github.com/BurntSushi/wingo/...+  go get -v github.com/BurntSushi/gohead/...++  virtualenv2 "$HOME/.pyx11"+  . "$HOME/.pyx11/bin/activate"+  pip install pywingo+}++gpginit() {+  rscp nas:/m/data/andrew/keys/*.key.* /tmp/+  gpg --import /tmp/jamslam@gmail.com.key.priv+  gpg --import /tmp/kbrady39@gmail.com.key.priv+  echo "use 'gpg --edit-key {email}' to trust each key completely"+}++sshinit() {+  ssh-extract-key burntsushi+}++scannerinit() {+  brsaneconfig4 -a name=Brother model=DCP-7065DN ip=192.168.1.150+}++autologin() {+  sudo mkdir -p /etc/systemd/system/getty@tty1.service.d+  sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf > /dev/null <<EOF+[Service]+ExecStart=+ExecStart=-/usr/bin/agetty --autologin andrew --noclear %I $TERM+EOF+}++nmmedia() {+  script="/etc/NetworkManager/dispatcher.d/10-media.sh"+  sudo tee "$script" > /dev/null <<EOF+#!/bin/sh++status="\$2"+case "\$status" in+  up)+    sudo -i -u andrew zsh -c '. ~/.zshrc && media'+    ;;+  down)+    sudo -i -u andrew zsh -c '. ~/.zshrc && media down'+    ;;+esac+EOF+  sudo chmod 0755 "$script"+}++case "$1" in+  bootstrap) shift ; bootstrap "$@" ;;+  rootinit) shift ; rootinit "$@" ;;+  x) shift ; x "$@" ;;+  pkgs) shift ; pkgs "$@" ;;+  xpkgs) shift ; xpkgs "$@" ;;+  userinit) shift ; userinit "$@" ;;+  kaitinit) shift ; kaitinit "$@" ;;+  grubinit) shift ; grubinit "$@" ;;+  yayinit) shift ; yayinit "$@" ;;+  yaypkgs) shift ; yaypkgs "$@" ;;+  yayxpkgs) shift ; yayxpkgs "$@" ;;+  homeinit) shift ; homeinit "$@" ;;+  xenvinit) shift ; xenvinit "$@" ;;+  gpginit) shift ; gpginit "$@" ;;+  sshinit) shift ; sshinit "$@" ;;+  scannerinit) shift ; scannerinit "$@" ;;+  autologin) shift ; autologin "$@" ;;+  nmmedia) shift ; nmmedia "$@" ;;+  *)+    echo "unrecognized command" >&2+    exit 1+    ;;+esac

bin/autobright

new file mode 100755@@ -0,0 +1,13 @@+#!/bin/sh++status="$(cat /sys/class/power_supply/BAT0/status)"+if [ "$status" != "Discharging" ]; then+  brightness ac+else+  brightness bat+fi+# if on_ac_power; then+#   brightness ac+# else+#   brightness bat+# fi

bin/avg-lines

new file mode 100755@@ -0,0 +1,13 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function+import numpy as np+import sys++xs = []+for line in sys.stdin:+    line = line.strip()+    if len(line) == 0:+        continue+    xs.append(float(line))+print('%f +/- %f' % (np.average(xs), np.std(xs)))

bin/backup

new file mode 100755@@ -0,0 +1,378 @@+#!/bin/bash++# vim: tabstop=2 shiftwidth=2 softtabstop=2++# This shell script is a relatively simple tool for using s3 as a backup+# service based on some simple requirements:+#+#   - Some data is updated infrequently, but is large.+#   - Some data is updated more frequently, but is small.+#   - Some data is sensitive, e.g., tax information. Such data+#     should be encrypted *before* being sent to s3.+#+# This script addresses these requirements by introducing two different types+# of backup: "oneshot" and "sync."+#+# Oneshot backups take a single directory, turn it into a gzip archive, encrypt+# it and then upload it to s3://BUCKET/oneshot/$(date +'%Y-%m-%d'). This+# organizes the backups by day. This means you can only backup a directory at+# most once a day. This is OK because oneshot backups address the "big and+# infrequent" use case.+#+# Sync backups take a single directory, encrypt each file individually and then+# upload the resulting encrypted tree to s3 directly. There is no compression+# and the directory hierarchy itself is not concealed. This script WILL delete+# files in s3 that are no longer in the sync directory, so it is recommended+# to enable versioning on your bucket. (If you're worried about space, then+# set up a lifecycle rule to expire previous versions of files older than a+# particular age.)+#+# The sync command is intended to be run multiple times on the same directory.+# It won't re-upload or even re-encrypt every file every time. It isn't the+# fastest thing in the world, but it's fast enough to run once a day from a+# cronjob.+#+# Finally, this tool hard-codes knowledge of a particular directory hierarchy+# and prevents one from backing up directories that we know shouldn't be+# backed up. One can remove these checks in the `check_dir` function.++set -e++# The temporary directories to use while performing backups.+TMPDIR="/m/tmp"+# This directory is a placeholder for compressing and encrypting "one time"+# backups. This directory should be periodically cleaned.+TMPDIR_ONESHOT="$TMPDIR/oneshot"+# This directory contains three replicas of each directory that is synced.+# The first replica consists of md5 sums, which are updated every time a sync+# is started. The second replica consists of the actual files, but are+# encrypted. The third replica contains the md5 sums of the files in the+# encrypted replica *before* encryption. The replica containing the encrypted+# files is ultimately the one that is synced to s3. The other two replicas+# permit checking if a file needs to be re-encrypted. This makes the command+# idempotent.+#+# Note that sync does not require the existence of any replica directories.+# Ultimately, `aws s3 sync` will determine which files need to be synced; our+# scheme exists as a performance optimization to avoid re-encrypting data that+# hasn't changed.+#+# This scheme *does* expose the directory structure to s3, but each individual+# file is encrypted using our own keys.+TMPDIR_SYNC="$TMPDIR/sync"++# These are the three aforementioned replica directories.+TMPDIR_SYNC_PLAIN_SUMS="$TMPDIR_SYNC/plain-sums"+TMPDIR_SYNC_ENCRYPTED_SUMS="$TMPDIR_SYNC/encrypted-sums"+TMPDIR_SYNC_ENCRYPTED_FILES="$TMPDIR_SYNC/encrypted-files"++# Corresponding s3 paths.+S3_BUCKET="gallant-home"+S3_ONESHOT="s3://$S3_BUCKET/oneshot"+S3_SYNC="s3://$S3_BUCKET/sync"++usage() {+  echo "Usage: $(basename $0) [flags]" >&2+  echo "Flags:" >&2+  echo "  --clean        Clean the oneshot temporary directories." >&2+  echo "  --list         List all oneshot backups. This is the default." >&2+  echo "  --summary      List all oneshot backups and show a summary." >&2+  echo "  --oneshot DIR  Do a oneshot backup of given directory." >&2+  echo "  --sync DIR     Sync the given directory to s3, with client side encryption." >&2+  exit 1+}++cmd_list() {+  aws s3 ls --recursive --human-readable "$S3_ONESHOT"+}++cmd_summary() {+  aws s3 ls --recursive --human-readable --summarize "$S3_ONESHOT"+}++cmd_clean() {+  today=$(date +'%Y-%m-%d')+  for dir in "$TMPDIR_ONESHOT"/*/; do+    if ! [ -d "$dir" ]; then+      continue+    fi+    # Delete any directory older than 1 day.+    dir_day="$(basename "$dir")"+    if [ $(daydiff "$today" "$dir_day") -ge 2 ]; then+      echo "# removing $dir" >&2+      rm -rf "$dir"+    fi+  done+}++# Execute the oneshot backup strategy on a single directory.+#+# All we really do here is create a gzipped tar archive of the given+# directory in a temporary directory, encrypt it and then upload it.+#+# Since the data could be quite large, we don't use /tmp, which is probably+# a ramdisk. We don't clean up after ourselves immediately, but we will clean+# aup nything that's older than a day.+cmd_oneshot() {+  if [ $# != 1 ]; then+    usage+  fi++  today=$(date +'%Y-%m-%d')+  tmpdir="$TMPDIR_ONESHOT/$today"++  backup_dir="$1"+  if ! [ -d "$backup_dir" ]; then+    echo "$backup_dir is not a directory" >&2+    exit 1+  fi++  backup_dir="$(realpath "$backup_dir")"+  backup_parent="$(dirname "$backup_dir")"+  backup_name="$(basename "$backup_dir")"+  check_dir "$backup_dir"++  echo "===== started oneshot backup of $backup_dir at $(date)" >&2++  backup_tmpdir="$tmpdir/$backup_name"+  mkdir -p "$backup_tmpdir"++  archive="$backup_tmpdir/$backup_name.tar.gz"+  gpg_archive_name="$backup_name.tar.gz.gpg"+  gpg_archive="$backup_tmpdir/$gpg_archive_name"+  if ! [ -f "$archive" ]; then+    echo "# creating compressed archive of $backup_dir" >&2+    tar zcf "$archive" -C "$backup_dir/.." "$backup_name"+  fi+  if ! [ -f "$gpg_archive" ]; then+    echo "# encrypting $archive" >&2+    encrypt_file "$archive" "$gpg_archive"+  fi++  s3_path_prefix="$S3_ONESHOT/$today"+  s3_path="$s3_path_prefix/${backup_parent#/}/$gpg_archive_name"+  echo "# uploading to s3 $s3_path" >&2+  aws s3 cp $no_aws_progress \+    --storage-class STANDARD_IA "$gpg_archive" "$s3_path"++  echo "# cleaning temporary dir" >&2+  "$0" --clean++  echo "===== finished oneshot backup of $backup_dir at $(date)" >&2+}++# Execute the sync backup strategy on a single directory.+#+# Normally, this would be a simple call to `aws s3 sync`, but since we want to+# encrypt our data before sending it to s3, we need to re-implement parts of+# the sync itself. In particular, since we might run this on large directory+# trees, we don't want to re-encrypt every file every time the command is run.+# Instead, we track the md5 sums of files just before they are encrypted, and+# compare those with the md5 sums of the current files. We respond accordingly+# by adding new encrypted files, re-encrypting files or deleting encrypting+# files.+#+# Ultimately, we don't sync the given directory directly. Instead, we sync a+# replica of the directory that mirrors it, except that each file has been+# encrypted.+#+# Note that there are some races in this logic. For example, a file might+# change after we record its md5 but before we compare it with the previous+# md5. This is OK; we will pick up the change on the next sync automatically.+cmd_sync() {+  if [ $# != 1 ]; then+    usage+  fi++  sync_dir="$(realpath "$1")"+  check_dir "$sync_dir"++  echo "===== starting sync of $sync_dir at $(date)" >&2++  # Create the directory structure for each replica.+  rm -rf "$TMPDIR_SYNC_PLAIN_SUMS$sync_dir"+  find "$sync_dir" -type d | while read -r dir; do+    mkdir -p \+      "$TMPDIR_SYNC_PLAIN_SUMS$dir" \+      "$TMPDIR_SYNC_ENCRYPTED_SUMS$dir" \+      "$TMPDIR_SYNC_ENCRYPTED_FILES$dir"+  done++  old_md5=$(mktemp)+  old_files=$(mktemp)+  new_md5=$(mktemp)+  new_files=$(mktemp)++  # Find the md5 sums of the current files.+  find "$sync_dir" -type f -print0 \+    | xargs -0 -n512 -P4 md5sum | \+    while read -r md5 path; do+      echo -e "$path\t$md5"+    done \+    | sort > "$new_md5"+  cut -f1 "$new_md5" | sort > "$new_files"++  # Find the md5 sums of the files that have been encrypted.+  # (Note that these are the md5 sums of the file just before encryption.)+  find "$TMPDIR_SYNC_ENCRYPTED_SUMS$sync_dir" -name '*.md5' -type f | \+    while read -r path; do+      orig_path_md5="${path#$TMPDIR_SYNC_ENCRYPTED_SUMS}"+      orig_path="${orig_path_md5%%.md5}"+      echo -e "$orig_path\t$(cat "$path")"+    done \+    | sort > "$old_md5"+  cut -f1 "$old_md5" | sort > "$old_files"++  # Find everything in the current set that isn't in the old set and add it.+  comm -13 "$old_files" "$new_files" \+    | xargs --no-run-if-empty -d'\n' -P4 -n128 \+        "$0" --internal-encrypt-and-sum adding+  # Find everything in the old set that isn't in the current set and remove it.+  comm -23 "$old_files" "$new_files" | \+    while read -r path; do+      echo "# removing $path" >&2+      rm -f "$TMPDIR_SYNC_ENCRYPTED_FILES$path.gpg"+      rm -f "$TMPDIR_SYNC_ENCRYPTED_SUMS$path.md5"+    done+  # Find md5 disagreements and re-encrypt the file.+  join -t$'\t' "$old_md5" "$new_md5" | \+    while IFS=$'\t' read -r path old new; do+      if [[ "$old" != "$new" ]]; then+        echo "$path"+      fi+    done \+    | xargs --no-run-if-empty -d'\n' -P4 -n128 \+        "$0" --internal-encrypt-and-sum updating+  # Clean up after ourselves.+  rm -f "$old_md5" "$old_files" "$new_md5" "$new_files"++  # Sync to s3.+  aws s3 sync --delete $no_aws_progress \+    "$TMPDIR_SYNC_ENCRYPTED_FILES$sync_dir" "$S3_SYNC$sync_dir"++  echo "===== finished sync of $sync_dir at $(date)" >&2+}++# This is an internal command that we use to encrypt a file and update its+# md5 sum. We define it this way so that we can parallelize its execution with+# xargs.+cmd_internal_encrypt_and_sum() {+  if [ $# -lt 2 ]; then+    echo "Usage: $(basename $0) --internal-encrypt-and-sum action path [ path ... ]" >&2+    exit 1+  fi+  action="$1"+  shift++  for path; do+    echo "# $action $path" >&2+    encrypt_file "$path" "$TMPDIR_SYNC_ENCRYPTED_FILES$path.gpg"+    md5sum "$path" | awk '{print $1}' > "$TMPDIR_SYNC_ENCRYPTED_SUMS$path.md5"+  done+}++# Compute the difference between two datestamps, in seconds.+daydiff() {+  date1="$(date -d "$1" +'%s')"+  date2="$(date -d "$2" +'%s')"+  echo $(((date1 - date2) / (60 * 60 * 24)))+}++# Enforce some sanity checks on directories given by user.+#+# Technically, these aren't necessary in general. They're intended to enforce+# my own personal convention.+check_dir() {+  dir="$1"++  if ! [[ $dir = /m/* ]]; then+    echo "$dir is not a descendent of /m/" >&2+    exit 1+  fi++  blacklist=(media git old-snapshots isos tmp log sets)+  for name in "${blacklist[@]}"; do+    if [[ $dir = /m/$name ]]; then+      echo "/m/$name cannot be backed up" >&2+      exit 1+    fi+  done+}++# Convenience function for shelling out to gpg.+encrypt_file() {+  in="$1"+  out="$2"+  set +e+  gpg --yes --encrypt \+    --output "$out" \+    --recipient jamslam@gmail.com \+    --recipient kbrady39@gmail.com \+    "$in" \+    2>&1 | grep -E -v 'gpg: automatically retrieved'+}++# Convenience function for checking that a command exists.+requires() {+  cmd="$1"+  if ! command -v "$cmd" > /dev/null 2>&1; then+    echo "DEPENDENCY MISSING: $(basename $0) requires $cmd to be installed" >&2+    exit 1+  fi+}++# When set, clean the backup tmp directory and exit.+clean=+# When set, list available backups and exit.+list=+# When set, list available backups with a summary and exit.+summary=+# When set, sync the directory to s3.+sync=+# When set, do a oneshot backup keyed on today's date.+oneshot=+# When set, do not show aws upload progress.+no_aws_progress=++# For internal use.+internal_encrypt_and_sum=++# Dependencies.+requires aws+requires gpg+requires egrep+requires xargs+requires md5sum+requires date+requires comm+requires join++while [ $# -gt 0 ]; do+  case "$1" in+    --clean) clean=yes ; shift ;;+    --list) list=yes ; shift ;;+    --summary) summary=yes ; shift ;;+    --oneshot) oneshot=yes ; shift ;;+    --sync) sync=yes ; shift ;;+    --no-aws-progress) no_aws_progress=--quiet ; shift ;;+    --internal-encrypt-and-sum) internal_encrypt_and_sum=yes ; shift ;;+    -h|-*) usage ;;+    *) break ;;+  esac+done++if [ -n "$internal_encrypt_and_sum" ]; then+  cmd_internal_encrypt_and_sum "$@"+elif [ -n "$list" ]; then+  cmd_list+elif [ -n "$summary" ]; then+  cmd_summary+elif [ -n "$clean" ]; then+  cmd_clean+elif [ -n "$sync" ]; then+  cmd_sync "$@"+elif [ -n "$oneshot" ]; then+  cmd_oneshot "$@"+else+  cmd_summary+fi

bin/batrem

new file mode 100755@@ -0,0 +1,125 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function++import argparse+import os+import os.path+import subprocess+import sys+import time+++def eprint(*args, **kwargs):+    kwargs['file'] = sys.stderr+    print(*args, **kwargs)+++def bat_value(fname):+    f = os.path.join(batdir, fname)+    if not os.access(f, os.R_OK):+        eprint('Could not access "%s".' % f)+        sys.exit(1)+    val = 0.0+    try:+        val = float(open(f).read())+    except IOError:+        eprint('Could not open "%s".' % f)+        sys.exit(1)+    except ValueError:+        eprint(+            'The value "%s", returned from "%s", is not a valid float.'+            % (open(f).read(), f))+    return val+++def output():+    full, now, pow_now = bat_value(ffull), bat_value(fnow), bat_value(fpow_now)++    charged = '%d' % (100 * (now / full))++    # If the battery isn't discharging, just report 'AC' instead of+    # the time remaining.+    status = open(os.path.join(batdir, 'status')).read().strip()+    if status != 'Discharging':+        print('%s%% AC' % charged)+        sys.stdout.flush()+        return++    seconds_rem = (now * 60 * 60) / pow_now+    minutes_rem = '%d' % ((seconds_rem / 60) % 60)+    hours_rem = '%d' % (seconds_rem / 60 / 60)++    def rem():+        if conf.short_rem:+            return '%hh%mm'+        return '%h hours, %m minutes'++    format_str = ''+    if conf.format_str:+        format_str = conf.format_str+    elif conf.output_type == 'charge':+        format_str = '%c%'+    elif conf.output_type == 'rem':+        format_str = rem()+    else:+        format_str = rem() + ' %c%'+    assert(len(format_str) > 0)++    out = format_str+    out = out.replace('%c', charged)+    out = out.replace('%h', hours_rem)+    out = out.replace('%m', minutes_rem)+    print(out)+    sys.stdout.flush()+++if __name__ == '__main__':+    parser = argparse.ArgumentParser(+        description='Emit battery status charge level and time remaining.',+        formatter_class=argparse.ArgumentDefaultsHelpFormatter)+    aa = parser.add_argument+    aa('-f', '--format-str', type=str, metavar='FORMAT_STRING',+       help='A format string overrides the "-s" and "-t" options, and allows '+            'complete control over the formatting of the battery status '+            'message. Use "%%h" for hours, "%%m" for minutes and "%%c" for '+            'charge level. For example, using '+            '"%%h hours, %%m minutes (%%c%%)" a sample output '+            'might look like "5 hours, 16 minutes (66.90%%)".')+    aa('-i', '--interval', type=int, metavar='POLL_INTERVAL_SECONDS',+       help='The time, in seconds, between battery status polling. If this '+            'option is not specified, the current battery status will be '+            'printed once, and the program will exit.')+    aa('-s', '--short-rem', action='store_true',+       help='When enabled, the "time remaining" display will be shortened '+            'form "X hours, Y minutes" to "XhYm".')+    aa('-t', '--output-type', choices=['charge', 'rem'], metavar='OUTPUT_TYPE',+       help='The type of output to display. If this option is omitted, both '+            'the charge level and the time remaining are displayed. If '+            '"charge" is specified, only the charge level is displayed. If '+            '"rem" is specified, only the time remaining is displayed.')+    conf = parser.parse_args()++    host = subprocess.check_output(['hostname']).strip()+    batdir = ''+    ffull = 'energy_full'+    fnow = 'energy_now'+    fpow_now = 'power_now'++    if host in ('Tiger', 'Turtle', 'monty', 'krusty'):+        batdir = '/sys/class/power_supply/BAT0'+    if host in ('krusty'):+        ffull = 'charge_full'+        fnow = 'charge_now'+        fpow_now = 'current_now'++    if not os.path.isdir(batdir):+        eprint('Battery directory "%s" is not accessible.' % batdir)+        sys.exit(1)++    if conf.interval > 0:+        while True:+            output()+            time.sleep(conf.interval)+    else:+        output()

bin/batwin

new file mode 100755@@ -0,0 +1,20 @@+#!/bin/sh++case "$(hostname)" in+monty)+  killall dzen2+  exec batrem -s -i 10 | dzen2 -fn "DejaVu Sans Mono-14" \+    -fg white -bg black \+    -x 1335 -y 860 -w 115 -h 40+  ;;+krusty)+  killall dzen2+  exec batrem -s -i 10 | dzen2 -fn "DejaVu Sans Mono-14" \+    -fg white -bg black \+    -x 1655 -y 1040 -w 115 -h 40+  ;;+*)+  echo "Unrecognized machine '$(hostname)'." >&2+  exit 1+  ;;+esac

bin/brightness

new file mode 100755@@ -0,0 +1,19 @@+#!/bin/sh++case $1 in+  bat)+    xbacklight -set 40+    ;;+  ac)+    xbacklight -set 100+    ;;+  inc)+    xbacklight -inc 20+    ;;+  dec)+    xbacklight -dec 20+    ;;+  *)+    echo "Unrecognized configuration '$1'." >&2+    exit 1+esac

bin/c

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec column -t -s'	' "$@"

bin/cargo-up

new file mode 100755@@ -0,0 +1,101 @@+#!/bin/bash++set -e++function update_version {+  manifest="$1"+  semver="$2"++  sed -i "s/^version = \".*\"\s\+#:version$/version = \"$semver\"  #:version/g" "$manifest"+}++function current_version {+  manifest="$1"+  grep '^version' "$manifest" | sed 's/.*"\([0-9]\+\.[0-9]\+\.[0-9]\+\)"/\1/g'+}++function next_version {+  cur="$1"+  nopatch=${cur%.*}+  patch=${cur##*.}+  patchplus=$((patch + 1))+  printf "%s.%s\n" $nopatch $patchplus+}++function pkg_name {+  manifest="$1"+  grep -E '^name = "[^"]+"$' "$manifest" \+    | head -n1 \+    | grep -Po '(?<=name = ")[^"]+'+}++release=yes+push=yes+while true; do+  case "$1" in+    --no-release) release= && shift ;;+    --no-push) push= && shift ;;+    -*)+      echo "Usage: $(basename $0) [ --no-release --no-push ] [ path/to/Cargo.toml [ semver ] ]" >&2+      exit 1+      ;;+    *) break ;;+  esac+done++case $# in+  0)+    manifest=Cargo.toml+    semver=$(next_version $(current_version "$manifest"))+    ;;+  1)+    manifest="$1"+    semver=$(next_version $(current_version "$manifest"))+    ;;+  2)+    manifest="$1"+    semver="$2"+    ;;+  *)+    echo "Usage: $(basename $0) [ --no-release ] [ path/to/Cargo.toml [ semver ] ]" >&2+    exit 1+    ;;+esac++if [ -n "$(git status --untracked-files=no --porcelain)" ]; then+  echo "Git working directory is not clean." >&2+  exit 1+fi+if ! grep -q '#:version' "$manifest"; then+  echo "Could not find '#:version' tag in $manifest" >&2+  exit 1+fi++update_version "$manifest" "$semver"+pkg="$(pkg_name "$manifest")"+prefix="$pkg-"+# prefix="$(basename $(dirname "$manifest"))-"+if [ "$prefix" = ".-" ]; then+  prefix=+fi+tagname="$prefix$semver"+if [ -f Cargo.toml ] && grep -E -q '\[\[bin\]\]' Cargo.toml; then+  cargo update -p "$pkg"+fi+git commit -a -m "$tagname"+git tag -s -a "$tagname" -m "$tagname"++if [ -n "$release" ]; then+  (+    cd "$(dirname "$manifest")"+    cargo publish+  )+  if [ -n "$push" ]; then+    for remote in origin github home; do+      if git remote | grep -q "$remote"; then+        git push "$remote" master+        git push "$remote" "$tagname"+      fi+    done+  fi+fi

bin/ci

new file mode 100755@@ -0,0 +1,12 @@+#!/bin/sh++if [ $# -gt 1 ]; then+  echo "Usage: $(basename "$0") [path]" >&2+  exit 1+fi++path="$1"+if [ -z "$path" ]; then+  path="."+fi+cargo install -f --path "$path"

bin/clean-media-folders

new file mode 100755@@ -0,0 +1,13 @@+#!/bin/bash++for d; do+  if ! [ -d "$d" ]; then+    echo "$d is not a directory. skipping." >&2+    continue+  fi+  mv "$d"/*.{mkv,avi,mp4} ./ 2>/dev/null+  rm "$d"/*.{txt,nfo,jpg,exe} 2>/dev/null+  rm -rf "$d"/Sample 2>/dev/null+  rm -rf "$d"/Screens 2>/dev/null+  rmdir "$d"+done

bin/clear-agents

new file mode 100755@@ -0,0 +1,11 @@+#!/bin/sh++# Clear the gpg agent's cached passwords. It has a TTL of 1 day, but we should+# clear them whenever we lock the screen too.+if cmd-exists gpgconf; then+    gpgconf --reload gpg-agent+fi+# And clear ssh agent keys as well. Use ssh-add-keys to re-add them.+if cmd-exists ssh-add; then+    ssh-add -D+fi

bin/clear-file-cache

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++set -e++/usr/bin/sync+sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

bin/clipme

new file mode 100755@@ -0,0 +1,14 @@+#!/bin/bash++if [ "$1" = "shrug" ]; then+    toclip="¯\\_(ツ)_/¯"+elif [ "$1" = "snowman" ]; then+    toclip="☃"+elif [ "$1" = "sherlock-ru" ]; then+    toclip="Шерлок Холмс"+else+    echo "$(basename "$0") (shrug | snowman | sherlock-ru)" >&2+    exit 1+fi++echo -n "$toclip" | xclip -selection clipboard

bin/cmd-exists

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++if [ ! $# -eq 1 ]; then+  echo "usage: $(basename $0) command"+fi+command -v $1 > /dev/null 2>&1

bin/colcheck

new file mode 100755@@ -0,0 +1,86 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function+import argparse+from collections import OrderedDict+import os+import sys+++def eprint(*args, **kwargs):+    kwargs['file'] = sys.stderr+    print(*args, **kwargs)+++parser = argparse.ArgumentParser(+    description='Detect lines longer than 79 columns')+aa = parser.add_argument+aa(dest='files', metavar='files', nargs='*')+aa('-c', '--columns', dest='cols', type=int, default=79)+aa('-t', '--tab-width', dest='tabw', type=int, default=4)+aa('--no-color', dest='no_color', action='store_true')+args = parser.parse_args()++try:+    if args.no_color:+        def colored(text, *args, **kwargs):+            return text+    else:+        from termcolor import colored+except ImportError:+    def colored(text, *args, **kwargs):+        return text++C = args.cols+labelFmt = '%s[%d]: '  # filename and line number++fileErrors = OrderedDict()  # { filename -> errors }+# errors :: [(line number, obedient text, overflow text)]+++def errorColor(s):+    return colored(s, 'white', on_color='on_red')+++def validColor(s):+    return colored(s, 'green', attrs=['bold'])+++def tabsToSpaces(line):+    return line.replace('\t', ' ' * args.tabw)+++for f in args.files:+    if f in fileErrors:+        continue+    if not os.access(f, os.R_OK):+        eprint('Could not read file "%s"' % f)+        continue++    fileErrors[f] = []+    for i, line in enumerate(open(f), 1):+        line = tabsToSpaces(line)++        if line[-1] == '\n':+            line = line[:-1]+        if len(line) > C:+            fileErrors[f].append((i, line[:C], errorColor(line[C:])))++someErrors = False+for f, errors in fileErrors.iteritems():+    # File heading+    if not errors:+        continue++    someErrors = True+    print(errorColor('%s has lines exceeding %d columns:' % (f, C)))++    # Some formatting: find the longest string in 'filename[line#]'+    labelMax = max(map(lambda (i, g, b): len(labelFmt % (f, i)), errors))+    for (i, good, bad) in errors:+        label = (labelFmt % (f, i)).ljust(labelMax)+        print('%s%s%s' % (label, good, bad))+    print()++if someErrors:+    sys.exit(1)

bin/compile-ripgrep

new file mode 100755@@ -0,0 +1,74 @@+#!/usr/bin/env zsh++checkout="$HOME/rust/ripgrep"+custom=+debug=+# features='simd-accel'+features=''+target=''+target_flag=''++while [ $# -gt 0 ]; do+    case "$1" in+    --debug)+        debug=1+        shift+        ;;+    --pcre2)+        features+=' pcre2'+        shift+        ;;+    --musl)+        target='x86_64-unknown-linux-musl'+        target_flag=("--target" $target)+        shift+        ;;+    --help)+        echo "Usage: $(basename "$0") [--debug --pcre2 --musl]"+        exit+        ;;+    -*)+        break+        ;;+    *)+        checkout="$1"+        custom=1+        shift+        ;;+    esac+done++export RUSTFLAGS="-C target-cpu=native"+# export RUSTFLAGS="-C target-cpu=native -C link-arg=-fuse-ld=lld"+# export RUSTFLAGS="--cfg regex_disable_auto_optimizations --cfg regex_runtime_teddy_ssse3"+if [ -n "$debug" ]; then+  cargo build "$@" \+    --manifest-path "$checkout/Cargo.toml" \+    --features "$features" \+    $target_flag+else+  cargo build "$@" \+    --manifest-path "$checkout/Cargo.toml" \+    --features "$features" \+    --release \+    $target_flag+fi+if [ -z "$custom" ]; then+  if [ -d "$HOME/man" ]; then+    manfile="$(+      find "$checkout/target" -name rg.1 -print0 \+        | xargs -0 ls -t \+        | head -n1+    )"+    if [ -f "$manfile" ]; then+      mkdir -p "$HOME/man/man1"+      cp "$manfile" "$HOME/man/man1/rg.1"+    fi+  fi+  if [ -d "$HOME/.zsh-complete" ]; then+      cp "$checkout/complete/_rg" "$HOME/.zsh-complete/_rg"+  fi+  if [ -n "$target" ] && [ -z "$debug" ]; then+      cp "$checkout/target/$target/release/rg" "$checkout/target/release/rg"+  fi+fi

bin/configure-git

new file mode 100755@@ -0,0 +1,5 @@+#!/bin/sh++git config user.name 'Andrew Gallant'+git config user.email 'jamslam@gmail.com'+git config user.signingkey 'jamslam@gmail.com'

bin/crate-version-tag

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++if [ $# != 1 ]; then+    echo "Usage: $(basename $0) tag" >&2+    exit 1+fi++tag="$1"+exec git tag -s -a "$tag" -m "$tag"

bin/cron-backup

new file mode 100755@@ -0,0 +1,19 @@+#!/bin/zsh++export HOME=/home/andrew+. $HOME/.zshrc++LOG=/m/log/backup.log++no_aws_progress=+while [ $# -gt 0 ]; do+  case "$1" in+    --no-aws-progress) no_aws_progress=--no-aws-progress ; shift ;;+    -h|-*)+      echo "Usage: $(basename $0) [--no-aws-progress]" >&2+      exit+      ;;+  esac+done++cmail backup $no_aws_progress --sync /m/data 2>&1 | tee -a "$LOG"

bin/csv-count

new file mode 100755@@ -0,0 +1,24 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function+import argparse+import csv+import sys++p = argparse.ArgumentParser(+    description='Count the number of records in the given CSV file.')+p.add_argument('file', metavar='FILE')+p.add_argument('-d', metavar='DELIMITER', default=',')+args = p.parse_args()++csv.field_size_limit(sys.maxsize)+count = 0+rdr = csv.reader(open(args.file), delimiter=args.d)+try:+    for i, row in enumerate(rdr):+        count += 1+except Exception as e:+    print(e, file=sys.stderr)+    print('Current count: %d' % count)+    sys.exit(1)+print(count)

bin/csv-count3

new file mode 100755@@ -0,0 +1,24 @@+#!/usr/bin/env python3++from __future__ import absolute_import, division, print_function+import argparse+import csv+import sys++p = argparse.ArgumentParser(+    description='Count the number of records in the given CSV file.')+p.add_argument('file', metavar='FILE')+p.add_argument('-d', metavar='DELIMITER', default=',')+args = p.parse_args()++csv.field_size_limit(sys.maxsize)+count = 0+rdr = csv.reader(open(args.file, encoding='utf-8'), delimiter=args.d)+try:+    for i, row in enumerate(rdr):+        count += 1+except Exception as e:+    print(e, file=sys.stderr)+    print('Current count: %d' % count)+    sys.exit(1)+print(count)

bin/csv-equal

new file mode 100755@@ -0,0 +1,44 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function+import argparse+import csv+import itertools+import sys++csv.field_size_limit(sys.maxsize)+++def delimiter_for(fname, default=None):+    if fname.endswith('.tsv'):+        return '\t'+    elif fname.endswith('.csv'):+        return ','+    elif default is not None:+        return default+    else:+        raise ValueError("Could not find delimiter for '%s'" % fname)+++if __name__ == '__main__':+    p = argparse.ArgumentParser()+    p.add_argument('csv_file1')+    p.add_argument('csv_file2')+    p.add_argument('-d', metavar='DELIMITER', default=None)+    args = p.parse_args()++    delimit1 = delimiter_for(args.csv_file1, default=args.d)+    reader1 = csv.reader(open(args.csv_file1), delimiter=delimit1)+    delimit2 = delimiter_for(args.csv_file2, default=args.d)+    reader2 = csv.reader(open(args.csv_file2), delimiter=delimit2)+    for i, (row1, row2) in enumerate(itertools.izip(reader1, reader2)):+        if row1 != row2:+            print(row1)+            print(row2)+            for v1, v2 in zip(row1, row2):+                if v1 != v2:+                    print('CSV files differ at row %d' % i, file=sys.stderr)+                    print('"%s" != "%s"' % (v1, v2))+                    sys.exit(1)+            assert False, "unreachable (row: %d)" % i+    sys.exit(0)

bin/ctags-go

new file mode 100755@@ -0,0 +1,4 @@+#!/bin/bash++find ./ -path './vendor' -prune -or -name '*.go' -print0 \+  | xargs -0 gotags > tags

bin/ctags-rust

new file mode 100755@@ -0,0 +1,16 @@+#!/bin/bash++exec ctags -R \+  --options=$HOME/bin/ctags.rust \+  --languages=MyRust \+  --exclude=target++# From rusty-tags. Looks the same as ctags.rust, but recorded for posterity.+# .arg("--regex-Rust=/^[ \\t]*(#\\[[^\\]]\\][ \\t]*)*(pub[ \\t]+)?(extern[ \\t]+)?(\"[^\"]+\"[ \\t]+)?(unsafe[ \\t]+)?fn[ \\t]+([a-zA-Z0-9_]+)/\\6/f,functions,function definitions/")+# .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?type[ \\t]+([a-zA-Z0-9_]+)/\\2/T,types,type definitions/")+# .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?enum[ \\t]+([a-zA-Z0-9_]+)/\\2/g,enum,enumeration names/")+# .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?struct[ \\t]+([a-zA-Z0-9_]+)/\\2/s,structure names/")+# .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?mod[ \\t]+([a-zA-Z0-9_]+)\\s*\\{/\\2/m,modules,module names/")+# .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?(static|const)[ \\t]+([a-zA-Z0-9_]+)/\\3/c,consts,static constants/")+# .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?(unsafe[ \\t]+)?trait[ \\t]+([a-zA-Z0-9_]+)/\\3/t,traits,traits/")+# .arg("--regex-Rust=/^[ \\t]*macro_rules![ \\t]+([a-zA-Z0-9_]+)/\\1/d,macros,macro definitions/");

bin/ctags.rust

new file mode 100644@@ -0,0 +1,10 @@+--langdef=MyRust+--langmap=MyRust:.rs+--regex-MyRust=/^[ \t]*(#\[[^\]]\][ \t]*)*(pub[ \t]+)?(extern[ \t]+)?("[^"]+"[ \t]+)?(unsafe[ \t]+)?fn[ \t]+([a-zA-Z0-9_]+)/\6/p,functions,function definitions/+--regex-MyRust=/^[ \t]*(pub[ \t]+)?type[ \t]+([a-zA-Z0-9_]+)/\2/T,types,type definitions/+--regex-MyRust=/^[ \t]*(pub[ \t]+)?enum[ \t]+([a-zA-Z0-9_]+)/\2/g,enum,enumeration names/+--regex-MyRust=/^[ \t]*(pub[ \t]+)?struct[ \t]+([a-zA-Z0-9_]+)/\2/s,struct,structure names/+--regex-MyRust=/^[ \t]*(pub[ \t]+)?mod[ \t]+([a-zA-Z0-9_]+)/\2/m,modules,module names/+--regex-MyRust=/^[ \t]*(pub[ \t]+)?(static|const)[ \t]+([a-zA-Z0-9_]+)/\3/c,consts,static constants/+--regex-MyRust=/^[ \t]*(pub[ \t]+)?trait[ \t]+([a-zA-Z0-9_]+)/\2/t,traits,traits/+--regex-MyRust=/^[ \t]*macro_rules![ \t]+([a-zA-Z0-9_]+)/\1/d,macros,macro definitions/

bin/cwddirs

new file mode 100755@@ -0,0 +1,5 @@+#!/usr/bin/env sh++find ./ -mindepth 1 -maxdepth 1 -type d |+  sed 's#^\./##g' |+  grep -v '^\.'

bin/cx

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++chmod +x $*

bin/dist

new file mode 100755@@ -0,0 +1,9 @@+#!/usr/bin/env python++import statistics+import sys++data = [int(line.strip()) for line in sys.stdin]+print('median: %d' % statistics.median(data))+print('stdev: %d' % statistics.stdev(data))+print('mean: %d' % statistics.mean(data))

bin/dpms-settings

new file mode 100755@@ -0,0 +1,5 @@+#!/bin/sh++no-blanking+xset +dpms+xset dpms 0 0 600

bin/dunst-alert

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++/usr/bin/ogg123 /usr/share/sounds/KDE-Im-Nudge.ogg > /dev/null 2>&1

bin/duration

new file mode 100755@@ -0,0 +1,31 @@+#!/bin/sh++if [ $# != 1 ]; then+  echo "Usage: duration video-file" >&2+  exit 1+fi++fp="$1"+out="$(ffprobe -loglevel error -show_format "$fp")"++# shellcheck disable=SC2181+if [ $? != 0 ]; then+  echo "$out" >&2+  exit 1+fi++python2.7 <<EOF+from __future__ import division, print_function+import re++m = re.search('duration=(\d+)', '''$out''')+seconds = int(m.group(1))++hours = seconds // 3600+seconds -= hours * 3600++minutes = seconds // 60+seconds -= minutes * 60++print('%dh%dm%ds' % (hours, minutes, seconds))+EOF

bin/eip

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec curl 'https://ipinfo.io/ip' 2> /dev/null

bin/fix-perms

new file mode 100755@@ -0,0 +1,13 @@+#!/bin/sh++if [ "$1" = group ]; then+  find ./ -type f -print0 | xargs -0 chown $USER:users+  find ./ -type f -print0 | xargs -0 chmod 664+  find ./ -type d -print0 | xargs -0 chown $USER:users+  find ./ -type d -print0 | xargs -0 chmod 775+else+  find ./ -type f -print0 | xargs -0 chown $USER:users+  find ./ -type f -print0 | xargs -0 chmod 644+  find ./ -type d -print0 | xargs -0 chown $USER:users+  find ./ -type d -print0 | xargs -0 chmod 755+fi

bin/fix-perms-network

new file mode 100755@@ -0,0 +1,4 @@+#!/bin/sh++sudo find ./ -type d -print0 | xargs -0 sudo chmod 775+sudo find ./ -type f -print0 | xargs -0 sudo chmod 664

bin/fonts

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++fc-list | sed 's,:.*,,' | sort -u

bin/force-off

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++sudo poweroff --force

bin/force-reboot

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++sudo reboot --force

bin/genpass

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++pwgen --capitalize --numerals --secure --ambiguous "$@" 20 1

bin/gist

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++# Uploads a gist and copies to the resulting URL to the clipboard.+# (Instead of the primary X selection, which requires using the mouse. Bah.)+url=$(/usr/bin/gist "$@")+echo "$url" | xclip -selection clipboard+echo "$url"

bin/git-empty-last-commit

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++set -e+curr="$(git rev-parse HEAD)"++git reset HEAD~1+git commit --allow-empty --no-verify -C "$curr"

bin/github-latest-ripgrep

new file mode 100755@@ -0,0 +1,4 @@+#!/bin/sh++url="https://api.github.com/repos/BurntSushi/ripgrep/releases/latest"+curl -s "$url" | jq -r .tag_name

bin/google-chrome

new file mode 100755@@ -0,0 +1,5 @@+#!/bin/sh++# This used to do more when I used different versions of Chrome on different+# machines. But now I just use stable when I use Chrome.+exec google-chrome-stable "$@"

bin/gopath

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++if [ $# != 1 ]; then+  echo "Usage: $(basename $0) <path>" >&2+  exit 1+fi+p="$(readlink -f "$1")"+export GOPATH="$p"+export PATH="$p/bin:$PATH"

bin/gpd

new file mode 100755@@ -0,0 +1,30 @@+#!/bin/sh++set -e++force=++while true; do+  case "$1" in+    -f|--force)+      force=yes+      shift+      ;;+    -h|--help)+      echo "Usage: $(basename $0) [-f/--force] <branch>" >&2+      exit+      ;;+    *)+      break+      ;;+  esac+done++branch="$1"+for remote in home origin; do+  if [ -z "$force" ]; then+    git push $remote $branch+  else+    git pushf $remote $branch+  fi+done

bin/hrg

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec hyper-rg --engine hyperscan "$@"

bin/html-prettify

new file mode 100755@@ -0,0 +1,23 @@+#!/usr/bin/env python2++import argparse+import os+import sys++from bs4 import BeautifulSoup++parser = argparse.ArgumentParser(description='Prettify an HTML file')+aa = parser.add_argument+aa(dest='file', metavar='FILE', nargs='?', default=None)+args = parser.parse_args()++if args.file is None:+    args.file = sys.stdin+else:+    if not os.access(args.file, os.R_OK):+        print >> sys.stderr, 'Cannot read %s' % args.file+        sys.exit(1)+    args.file = open(args.file)++soup = BeautifulSoup(args.file, 'lxml')+print soup.prettify().encode('utf-8')

bin/http-rustdoc

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec darkhttpd /home/andrew/rust --port 8100 --default-mimetype image/svg+xml

bin/hub-rollup

new file mode 100755@@ -0,0 +1,32 @@+#!/bin/sh++if [ $# != 1 ]; then+  echo "Usage: $(basename "$0") <pr-number>" >&2+  exit 1+fi++pr="$1"+rebasing="$(git status | grep -q rebase && echo yes)"+branch="$(git rev-parse --abbrev-ref HEAD)"+inrollup="$(echo $branch | grep -q '^tmp/rollup/' && echo yes)"+if [ -n "$inrollup" ]; then+  branch="$(echo $branch | sed 's#^tmp/rollup/##g')"+fi+rollupbranch="tmp/rollup/$branch"+rolledup="$(git branch | grep -q " $rollupbranch" && echo yes)"++if [ -n "$rebasing" ]; then+  echo "please continue rebase in progress first" >&2+  exit 1+fi++set -ex+if [ -z "$rolledup" ]; then+  hub pr checkout "$pr" "$rollupbranch"+  git rebase "$branch"+fi+if git rev-parse --abbrev-ref HEAD | grep -q '^tmp/rollup/'; then+  git checkout "$branch"+fi+git merge --ff-only "$rollupbranch"+git branch -D "$rollupbranch"

bin/hub-select-pr

new file mode 100755@@ -0,0 +1,5 @@+#!/bin/sh++url="$(grep "/$1 " | awk '{print $1}')"+echo "$url" | xclip -selection clipboard+echo "$url"

bin/hub-show-prs

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec hub pr list --sort-ascending -f '%pC%U - %<(15)%au - %t%n'

bin/i3-volume

new file mode 100755@@ -0,0 +1,528 @@+#!/bin/bash+#+#  i3-volume+#+#  Volume control and volume notifications for i3wm.+#+#  Requires:+#    alsa-utils or pulseaudio-utils+#    awk (POSIX compatible)+#+#  Optional:+#    A libnotify compatible notification daemon such as notify-osd or dunst+#    notify-send (libnotify) or dunstify (dunst)+#+#  Copyright (c) 2016 Beau Hastings. All rights reserved.+#  License: GNU General Public License v2+#+#  Author: Beau Hastings <beausy@gmail.com>+#  URL: https://github.com/hastinbe/i3-volume+#+#  ---+#+#  Some tweaks were made by Andrew Gallant:+#+#  * Plays a sound when the volume is changed.+#  * Cleaned up lints to pass shellcheck.++# shellcheck disable=SC2155++# Play volume ding when it changes.+ding() {+    sound=/usr/share/sounds/freedesktop/stereo/audio-volume-change.oga+    if cmd-exists ogg123 && [ -f "$sound" ]; then+        if $opt_use_amixer; then+          ogg123 "$sound" > /dev/null 2>&1+        else+          sink="$(get_default_sink_name)"+          ogg123 -d pulse -o "sink:$sink" "$sound" > /dev/null 2>&1+        fi+    fi+}++# Get default sink name+get_default_sink_name() {+    pacmd stat | awk -F": " '/^Default sink name: /{print $2}'+}++# Get the volume as a percentage.+get_volume() {+    if $opt_use_amixer; then+        get_volume_amixer $card+    else+        get_volume_pulseaudio $sink+    fi+}++# Get the volume as a percentage.+#+# Arguments+#   Sink name   (string) Symbolic name of sink.+get_volume_pulseaudio() {+    local sink="$1"++    pacmd list-sinks |+        awk -W posix '/^[ \t]+name: /{insink = $2 == "<'$sink'>"}+                      /^[ \t]+volume: / && insink {gsub("%", ""); print $5; exit}'+}++# Get the volume as a percentage.+#+# Arguments+#   Card        (integer) Card number to control.+get_volume_amixer() {+    local card="$1"+    local volume++    if [ -n "$card" ]; then+        volume=$(amixer -c "$card" -- sget Master)+    else+        volume=$(amixer sget Master)+    fi++    echo $volume | awk -W posix -F'[][]' '/dB/ { gsub("%", ""); print $2 }'+}++# Increase volume relative to current volume.+#+# Arguments:+#   Step        (integer) Percentage to increase by.+raise_volume() {+    if $opt_use_amixer; then+        raise_volume_amixer "$card" "$1"+    else+        raise_volume_pulseaudio "$sink" "$1"+    fi+}++# Increase volume relative to current volume using pulseaudio.+#+# Arguments:+#   Sink name   (string)  Symbolic name of sink.+#   Step        (integer) Percentage to increase by.+raise_volume_pulseaudio() {+    local sink="$1"+    local step="${2:-5}"++    set_volume_pulseaudio "$sink" "+${step}%"+}++# Increase volume relative to current volume using amixer.+#+# Arguments:+#   Card        (integer) Card number to control.+#   Step        (integer) Percentage to increase by.+raise_volume_amixer() {+    local card="$1"+    local step="${2:-5}"++    set_volume_amixer "$card" "${step}%+"+}++# Decrease volume relative to current volume.+#+# Arguments:+#   Step        (integer) Percentage to decrease by.+lower_volume() {+    if $opt_use_amixer; then+        lower_volume_amixer "$card" "$1"+    else+        lower_volume_pulseaudio "$sink" "$1"+    fi+}++# Decrease volume relative to current volume using pulseaudio.+#+# Arguments:+#   Sink name   (string)  Symbolic name of sink.+#   Step        (integer) Percentage to decrease by.+lower_volume_pulseaudio() {+    local sink="$1"+    local step="${2:-5}"++    set_volume_pulseaudio "$sink" "-${step}%"+}++# Decrease volume relative to current volume using amixer.+#+# Arguments:+#   Card        (integer) Card number to control.+#   Step        (integer) Percentage to decrease by.+lower_volume_amixer() {+    local card="$1"+    local step="${2:-5}"++    set_volume_amixer "$card" "${step}%-"+}++# Set volume.+#+# Arguments:+#   Step        (integer) Percentage to decrease by.+set_volume() {+    if $opt_use_amixer; then+        set_volume_amixer "$card" "$1"+    else+        set_volume_pulseaudio "$sink" "$1"+    fi+}++# Set volume using pulseaudio.+#+# Arguments:+#   Sink name   (string) Symbolic name of sink.+#   Volume      (integer|linear factor|percentage|decibel)+set_volume_pulseaudio() {+    local sink="$1"+    local vol="$2"++    pactl set-sink-volume "$sink" "$vol" || pactl set-sink-volume "$sink" -- "$vol"+    ding+}++# Set volume using amixer.+#+# Arguments:+#   Card        (integer) Card number to control.+#   Volume      (integer|linear factor|percentage|decibel)+set_volume_amixer() {+    local card="$1"+    local vol="$2"++    if [ -n "$card" ]; then+        amixer -q -c "$card" -- set Master "$vol" unmute+    else+        amixer -q set Master "$vol" unmute+    fi+    ding+}++# Toggle mute.+toggle_mute() {+    if $opt_use_amixer; then+        toggle_mute_amixer "$card"+    else+        toggle_mute_pulseaudio "$sink"+    fi+}++# Toggle mute using pulseaudio.+#+# Arguments:+#   Sink name   (string) Symbolic name of sink.+toggle_mute_pulseaudio() {+    local sink="$1"++    pactl set-sink-mute "$sink" toggle+    ding+}++# Toggle mute using amixer.+#+# Arguments:+#   Card        (integer) Card number to control.+toggle_mute_amixer() {+    local card="$1"++    if [ -n "$card" ]; then+        amixer -q -c "$card" -- set Master toggle+    else+        amixer -q set Master toggle+    fi+    (+      # The sleep seems necessary when using+      # headphones, otherwise the "ding" never+      # actually makes it through. IDK why.+      sleep 0.1+      ding+    ) &+}++# Check if muted.+is_muted() {+    if $opt_use_amixer; then+        return $(is_muted_amixer "$card")+    else+        return $(is_muted_pulseaudio "$sink")+    fi+}++# Check if sink is muted.+#+# Arguments:+#   Sink name    (string) Symbolic name of sink.+#+# Returns:+#   0 when true, 1 when false.+is_muted_pulseaudio() {+    local sink="$1"++    muted=$(pacmd list-sinks |+                   awk -W posix '/^[ \t]+name: /{insink = $2 == "<'$sink'>"}+                                 /^[ \t]+muted: / && insink {print $2; exit}')+    [ "$muted" = "yes" ]+}++# Check if card is muted.+#+# Arguments:+#   Card        (integer) Card number to control.+#+# Returns:+#   0 when true, 1 when false.+is_muted_amixer() {+    local card="$1"+    local output++    if [ -n "$card" ]; then+        output=$(amixer -c "$card" -- sget Master)+    else+        output=$(amixer sget Master)+    fi++    status=$(echo $output | awk -W posix -F'[][]' '/dB/ { print $6 }')++    [ "$status" = "off" ]+}++# Gets an icon for the provided volume.+#+# Arguments:+#   Volume      (integer) An integer indicating the volume.+#+# Returns:+#   The volume icon name.+get_volume_icon() {+    local vol="$1"+    local icon++    if $opt_use_legacy_icons; then+        if [ "$vol" -ge 70 ]; then icon="audio-volume-high"+        elif [ "$vol" -ge 40 ]; then icon="audio-volume-medium"+        elif [ "$vol" -gt 0 ]; then icon="audio-volume-low"+        else icon="audio-volume-low"+        fi+    else+        if [ "$vol" -gt 100 ]; then icon="audio-volume-overamplified-symbolic.symbolic"+        elif [ "$vol" -ge 70 ]; then icon="audio-volume-high-symbolic.symbolic"+        elif [ "$vol" -ge 40 ]; then icon="audio-volume-medium-symbolic.symbolic"+        elif [ "$vol" -gt 0 ]; then icon="audio-volume-low-symbolic.symbolic"+        else icon="audio-volume-low-symbolic.symbolic"+        fi+    fi++    echo "${icon}"+}++# Display a notification indicating the volume is muted.+notify_muted() {+    local icon++    if $opt_use_legacy_icons; then+        icon="audio-volume-muted"+    else+        icon="audio-volume-muted-symbolic.symbolic"+    fi++    if $opt_use_dunstify; then+        dunstify -i $icon -t $expires -h int:value:0 -h string:synchronous:volume "Volume muted" -r 1000+    else+        notify-send -i $icon -t $expires -h int:value:0 -h string:synchronous:volume "Volume muted"+    fi+}++# Display a notification indicating the current volume.+notify_volume() {+    local vol=$(get_volume)+    local icon=$(get_volume_icon "$vol")+    local text="$(printf "Volume %03s%%" "$vol")"++    if $opt_show_volume_progress; then+        local progress=$(get_progress_bar "$vol")+        echo $progress+        text="$text $progress"+    fi++    if $opt_use_dunstify; then+        dunstify -i "$icon" -t $expires -h int:value:"$vol" -h string:synchronous:volume "$text" -r 1000+    else+        notify-send -i "$icon" -t $expires -h int:value:"$vol" -h string:synchronous:volume "$text"+    fi+}++# Updates the status line.+#+# Arguments:+#   signal  (string) The signal used to update the status line.+#   proc    (string) The name of the status line process.+update_statusline() {+    local signal="$1"+    local proc="$2"+    pkill "-$signal" "$proc"+}++# Generates a progress bar for the provided value.+#+# Arguments:+#   Percentage      (integer) Percentage of progress.+#   Maximum         (integer) Maximum percentage. (default: 100)+#   Divisor         (integer) For calculating the ratio of blocks to progress (default: 5)+#+# Returns:+#   The progress bar.+get_progress_bar() {+    local percent="$1"+    local max_percent=${2:-100}+    local divisor=${3:-5}+    local progress=$((+      ((percent > max_percent ? max_percent : percent) + divisor - 1) / divisor+    ))+    local remaining=$((+      (percent >= max_percent ? 0 : max_percent - percent) / divisor+    ))++    echo "percent: $percent, max: $max_percent, divisor: $divisor" >&2+    echo "progress: $progress, remaining: $remaining" >&2+    printf '█%.0s' $(eval echo "{1..$progress}")+    if [ $remaining -gt 0 ]; then+      printf ' %.0s' $(eval echo "{1..$remaining}")+    fi+}++# Display program usage.+usage() {+    echo "Usage: $0 [options]+Control volume and related notifications.++Options:+  -a                use alsa-utils instead of pulseaudio-utils for volume control+  -c <card>         card number to control (amixer only)+  -d <amount>       decrease volume+  -e <expires>      expiration time of notifications, in milliseconds+  -i <amount>       increase volume+  -l                use legacy icons+  -m                toggle mute+  -n                show notifications+  -p                show text volume progress bar+  -s <sink_name>    symbolic name of sink (pulseaudio only)+  -t <process_name> name of status line process. must be used with -u+  -u <signal>       update status line using signal. must be used with -t+  -v <value>        set volume+  -y                use dunstify instead of notify-send+  -h                display this help and exit+" 1>&2+    exit 1+}+###########################################################++opt_decrease_volume=false+opt_increase_volume=false+opt_mute_volume=false+opt_notification=false+opt_set_volume=false+opt_show_volume_progress=false+opt_use_amixer=false+opt_use_dunstify=false+opt_use_legacy_icons=false+card=""+signal=""+sink=""+statusline=""+volume=5+expires="1500"++while getopts ":ac:d:e:hi:lmnps:t:u:v:y" o; do+    case "$o" in+        a)+            opt_use_amixer=true+            ;;+        c)+            card="${OPTARG}"+            ;;+        d)+            opt_decrease_volume=true+            volume="${OPTARG}"+            ;;+        e)+            expires="${OPTARG}"+            ;;+        i)+            opt_increase_volume=true+            volume="${OPTARG}"+            ;;+        l)+            opt_use_legacy_icons=true+            ;;+        m)+            opt_mute_volume=true+            ;;+        n)+            opt_notification=true+            ;;+        p)+            opt_show_volume_progress=true+            ;;+        s)+            sink="${OPTARG}"+            ;;+        t)+            statusline="${OPTARG}"+            ;;+        u)+            signal="${OPTARG}"+            ;;+        v)+            opt_set_volume=true+            volume="${OPTARG}"+            ;;+        y)+            opt_use_dunstify=true+            ;;+        h | *)+            usage+            ;;+    esac+done+shift $((OPTIND-1)) # Shift off options and optional --++if ! $opt_use_amixer; then+    if [ -z $sink ]; then+        sink="$(get_default_sink_name)"+    fi+fi++if ${opt_increase_volume}; then+    raise_volume $volume+fi++if ${opt_decrease_volume}; then+    lower_volume $volume+fi++if ${opt_set_volume}; then+    set_volume $volume+fi++if ${opt_mute_volume}; then+    toggle_mute $sink+fi++# The options below this line must be last+if ${opt_notification}; then+    if is_muted; then+        notify_muted+    else+        notify_volume+    fi+fi++if [ -n "${signal}" ]; then+    if [ -z "${statusline}" ]; then+        usage+    fi+    update_statusline "${signal}" "${statusline}"+else+    if [ -n "${statusline}" ]; then+        usage+    fi+fi

bin/i3lock

new file mode 100755@@ -0,0 +1,4 @@+#!/bin/sh++clr=$(python3 -c "import random; print('%06x' % random.randint(0, 0xffffff))")+/usr/bin/i3lock --nofork --color "$clr" "$@"

bin/in-dir

new file mode 100755@@ -0,0 +1,10 @@+#!/bin/sh++die() {+  echo "$*" >&2; exit 1+}++[ -d "$1" ] || die "$1 is not a directory"+cd "$1" || die "Cannot cd to $1"+shift+exec "$@"

bin/is-cygwin

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/bash++if [[ "$(uname)" = *CYGWIN* ]]; then+    exit 0+fi+exit 1

bin/is-laptop

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++if [ -f "$HOME/.laptop" ]; then+    exit 0+fi+exit 1

bin/is-mac

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++if [ "$(uname)" = "Darwin" ]; then+    exit 0+fi+exit 1

bin/is-remote-old

new file mode 100755Binary file changed

bin/is-work

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++if [ -f "$HOME/.iswork" ]; then+  exit 0+else+  exit 1+fi

bin/kait-nas-sync

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++# It's important that the various local paths don't end with a slash.+rscp \+  "$HOME/Documents" \+  "$HOME/Music" \+  "$HOME/Pictures" \+  "$HOME/coursera" \+  nas:/m/data/kait/

bin/lock

new file mode 100755@@ -0,0 +1,20 @@+#!/bin/sh++# Be more sophisticated here. We don't always want to sleep for 8 seconds+# before forcing a 'standby'. We only want to do this if a lot of time has+# elapsed since the 'lock' has occurred. Perhaps some date arithmetic?+#+# Honestly, fuck this, just assign standby to a keybinding and be done with it.+#+# I have no idea what the above means. ---AG (3 years older)++set -e++if is-work || is-laptop; then+    clear-agents+fi+numlockx off+date >> /tmp/i3lock.log+i3lock >> /tmp/i3lock.log 2>&1 &+sleep 5+standby

bin/mark-to-rst

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++pandoc --from=markdown --to=rst "$@"

bin/media

new file mode 100755@@ -0,0 +1,27 @@+#!/bin/sh++case "$(hostname)" in+  nas)+    echo "cannot mount media on nas; the nas IS the media!" >&2+    exit 1+    ;;+  frink)+    if [ ! $1 ] || [ $1 != down ]; then+      sudo mount nas:/m /m+    else+      sudo umount /m+    fi+    ;;+  Turtle)+    msshfs down /m+    if [ ! $1 ] || [ $1 != down ]; then+      msshfs home.burntsushi.net:/m /m+    fi+    ;;+  *)+    msshfs down /m+    if [ ! $1 ] || [ $1 != down ]; then+      msshfs nas:/m /m+    fi+    ;;+esac

bin/memtop

new file mode 100755@@ -0,0 +1,11 @@+#!/bin/bash+# Display the top application memory users+# http://www.cyberciti.biz/faq/linux-check-memory-usage/#comment-51021++while read -r command percent rss; do+  if [[ "${command}" != "COMMAND" ]]; then+    rss="$(bc <<< "scale=2;${rss}/1024")"+  fi+  printf " %-26s%-8s%s\n" "${command}" "${percent}" "${rss} MB" \+  | sed 's/COMMAND/PROGRAM/' | sed 's/RSS MB/#MEM/'+done < <(ps -A --sort -rss -o comm,pmem,rss | head -n 10)

bin/mkcd

new file mode 100755@@ -0,0 +1,10 @@+#!/bin/sh++# This script needs to be sourced to be effective.++if [ $# != 1 ]; then+  echo "Usage: $(basename $0) <dir>" >&2+  exit 1+fi+mkdir -p "$1" || return+cd "$1" || return

bin/mp3-split

new file mode 100755@@ -0,0 +1,23 @@+#!/bin/sh++# TODO: install mp3splt+# For splitting an audiobook into pieces, so that you can use the seek bar+# sanely, `mp3splt` can be used in one run pretty much at the disk's speed. Its+# format language for metadata is atrocious, but thankfully putting the result+# into a script frees me from revisiting the horror.+#+# Here's the command I use to cut into 10-minute pieces without a 1-minute+# piece hanging at the end, and to name and title the pieces sequentially—so+# you can be spared the ordeal:+#+#   mp3splt -t '10.00>2.00' -o '@f @n2' -d . -g 'r%[@o,@N=1,@t=#t @N]' "$1"+#+# where $1 is the source file, and `.` is the current directory as the+# destination.+#+# IIRC it also comes with oggsplt and flacsplt—but no such luck for aac or ac3.+#+# See: https://news.ycombinator.com/item?id=22776613++echo "unimplemented" >&2+exit 1

bin/mp4-bad

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++for f in *; do+  if mp4info "$f" 2>&1 | grep -q "can't open"; then+    echo rm $f+  fi+done

bin/msshfs

new file mode 100755@@ -0,0 +1,15 @@+#!/bin/sh++if [ "$1" = down ]; then+  if cmd-exists fusermount3; then+    fusermount3 -u $2+  else+    fusermount -u $2+  fi+else+  if is-work; then+    sshfs $1 $2 -o allow_other,reconnect,follow_symlinks,kernel_cache+  else+    sshfs $1 $2 -o allow_other,reconnect,follow_symlinks,writeback_cache=no,kernel_cache+  fi+fi

bin/nas-git-create-bare

new file mode 100755@@ -0,0 +1,16 @@+#!/bin/sh++dir=/m/git+name="$1"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <repo-name>" >&2+    exit 1+fi++export GIT_DIR="$dir/$name"+if [ -d "$GIT_DIR" ]; then+    echo "$GIT_DIR already exists" >&2+    exit 1+fi+git init --bare --shared=group+git config --unset receive.denyNonFastforwards

bin/neovim-install-appimage

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++url="https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage"+path="$HOME/bin/x86_64/nvim"++curl -L "$url" --create-dirs -o "$path"+chmod +x "$path"

bin/netmon

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++watch -n 1 netstat -t -u

bin/nice-filesize

new file mode 100755@@ -0,0 +1,18 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function+import argparse+import sys++p = argparse.ArgumentParser()+p.add_argument('size', nargs='?', default=None, type=int,+               help='If not set, size is read from stdin.')+args = p.parse_args()++size = float(args.size if args.size is not None else int(sys.stdin.read()))+units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']+unit = 0+while size > 1024:+    size /= 1024+    unit += 1+print('%0.2f %s' % (size, units[unit]))

bin/no-blanking

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++# Blank everything out first and start fresh+xset s noblank+xset s noexpose+xset s 0 0+xset s off+xset dpms 0 0 0+xset -dpms

bin/no-blanking-loop

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++while true; do+  no-blanking+  sleep 60+done

bin/num-processors

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++if [ -r /proc/cpuinfo ]; then+  NPROC=$(grep -c '^processor' /proc/cpuinfo | tr -d ' ')+else+  NPROC=1+fi+echo $NPROC

bin/older-than

new file mode 100755@@ -0,0 +1,20 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function++import argparse+import os+import time+++if __name__ == '__main__':+    p = argparse.ArgumentParser()+    p.add_argument('seconds', type=int)+    p.add_argument('files', nargs='+')+    args = p.parse_args()++    now = time.time()+    for fpath in args.files:+        stat = os.stat(fpath)+        if now - stat.st_mtime > args.seconds:+            print(fpath)

bin/overwrite

new file mode 100755@@ -0,0 +1,25 @@+#!/bin/sh+# overwrite:  copy standard input to output after EOF+# (final version)++# set -x++case $# in+0|1)	echo 'Usage: overwrite file cmd [args]' 1>&2; exit 2+esac++file=$1; shift+new=/tmp/$$.new; old=/tmp/$$.old+trap 'rm -f $new $old; exit 1' 1 2 15	# clean up files++if "$@" >$new		# collect input+then+	cp $file $old	# save original file+	trap 'trap "" 1 2 15; cp $old $file	# ignore signals+	      rm -f $new $old; exit 1' 1 2 15	# during restore+	cp $new $file+else+	echo "overwrite: $1 failed, $file unchanged" 1>&2+	exit 1+fi+rm -f $new $old

bin/pacman-disowned

new file mode 100755@@ -0,0 +1,16 @@+#!/bin/bash++tmp=${TMPDIR-/tmp}/pacman-disowned-$UID-$$+db=$tmp/db+fs=$tmp/fs++mkdir "$tmp"+trap  'rm -rf "$tmp"' EXIT++pacman -Qlq | sort -u > "$db"++find /bin /etc /lib /sbin /usr \+  ! -name lost+found \+  \( -type d -printf '%p/\n' -o -print \) | sort > "$fs"++comm -23 "$fs" "$db"

bin/pacman-fix-keys

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++rm -rf /etc/pacman.d/gnupg+pacman-key --init+pacman-key --populate archlinux+pacman-key --refresh-keys

bin/par

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++LC_ALL=en_US.UTF-8 exec /usr/bin/par "$@"

bin/pdf

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec nohup okular "$@" > /dev/null 2>&1 &

bin/perf-stat

new file mode 100755@@ -0,0 +1,14 @@+#!/bin/bash++events="instructions,context-switches,major-faults,minor-faults"+events+=",cpu-cycles,bus-cycles,ref-cycles"+events+=",stalled-cycles-backend,stalled-cycles-frontend"+events+=",L1-icache-load-misses"+events+=",branches,branch-misses,cache-references,cache-misses"+events+=",mem-loads,mem-stores"+events+=",L1-dcache-loads,L1-dcache-load-misses,L1-dcache-prefetch-misses"+events+=",L1-dcache-stores,L1-dcache-store-misses"+events+=",LLC-loads,LLC-load-misses,LLC-prefetch-misses"+events+=",LLC-stores,LLC-store-misses"++perf stat -B -e "$events" sh -c "$(printf " %q" "$@")"

bin/perfr

new file mode 100755@@ -0,0 +1,37 @@+#!/bin/sh++REPORT_DIR="/tmp/perf-reports"++devnull=+callgraph=+while true; do+    case "$1" in+        --devnull) devnull=yes ; shift ;;+        --callgraph) callgraph=yes ; shift ;;+        -h|--help)+            echo "Usage: $(basename "$0") [--devnull --callgraph] command"+            exit 0+            ;;+        *)+            break+            ;;+    esac+done++mkdir -p "$REPORT_DIR"+f="$(mktemp "--tmpdir=$REPORT_DIR" XXXXXXXXXX.data)"+if [ -n "$devnull" ]; then+    if [ -n "$callgraph" ]; then+        perf record -F 10000 -o "$f" -g --call-graph dwarf "$@" > /dev/null+    else+        perf record -F 10000 -o "$f" -g "$@" > /dev/null+    fi+else+    if [ -n "$callgraph" ]; then+        perf record -F 10000 -o "$f" -g --call-graph dwarf "$@"+    else+        perf record -F 10000 -o "$f" -g "$@"+    fi+fi+echo "===== perf report created: $f ====="+exec perf report -M intel -i "$f"

bin/pg-schema-clean

new file mode 100755@@ -0,0 +1,30 @@+#!/bin/sh++dry_run=true+while [ $# -gt 0 ]; do+  case $1 in+    --do-it) dry_run= ; shift ;;+    *) break ;;+  esac+done+if [ $# != 1 ]; then+  echo "Usage: $(basename $0) [--do-it] PG_SCHEMA_NAME" >&2+  exit 1+fi++db="$1"+if [ -z "$dry_run" ]; then+  psql "$db" -c '\dn' \+    | awk '{print $1}' \+    | grep -E '^[a-z]' \+    | grep -E -v '^public$' \+    | xargs printf "DROP SCHEMA %s CASCADE;\n" \+    | while read -r sql; do psql "$db" -c "$sql"; done+else+  psql "$db" -c '\dn' \+    | awk '{print $1}' \+    | grep -E '^[a-z]' \+    | grep -E -v '^public$' \+    | xargs printf "DROP SCHEMA %s CASCADE;\n"+  echo "dry run complete; pass --do-it to execute" >&2+fi

bin/place

new file mode 100755@@ -0,0 +1,55 @@+#!/bin/sh++if [ $# != 5 ]; then+  echo "Usage: $(basename): monitor-number x y width height" >&2+  exit 1+fi++# Nice names for args.+monitor=$1+mx=$2+my=$3+mwidth=$4+mheight=$5++# Calculate the height of the "panel" and the y position of the "panel."+height=40+y=$((mheight - height + my))++# Width constants for the system tray and battery indicator.+tray_width=150+bat_width=115++# Determine the width of the pager based on the width of the system tray and+# battery indicator. If it's a laptop, include the battery indicator. Otherwise,+# leave it out.+if [ -f "$HOME/.laptop" ]; then+  pager_width=$((mwidth - bat_width - tray_width))+else+  pager_width=$((mwidth - tray_width))+fi++# Always kill the current pager and system tray.+# shellcheck disable=SC2009+ps aux | grep -v grep | grep dzen-workspaces | awk '{print $2}' | xargs -r kill+killall trayer++# Restart the pager and system tray with appropriate geometry.+# (pager-multihead -x $mx -y $y --width $pager_width --height $height &)++($HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \+  | dzen2 -dock -xs 2 -h 40 -fn 'Cousine-18' -ta l -bg '#ffffff' -y 1170) &+(trayer --widthtype pixel --width $tray_width \+        --heighttype pixel --height $height \+        --edge bottom --align right \+        --monitor $monitor \+        --SetDockType true &)++# If this is a laptop, kill the battery notifier and restart it too.+if [ -f "$HOME/.laptop" ]; then+  # shellcheck disable=SC2009+  ps aux | grep -v grep | grep batrem | awk '{print $2}' | xargs -r kill+  (batrem -s -i 10 | \+    dzen2 -fn "DejaVu Sans Mono-14" -fg white -bg black \+      -x $((mx + pager_width)) -y $y -w $bat_width -h $height &)+fi

bin/pre-rg

new file mode 100755@@ -0,0 +1,14 @@+#!/bin/bash++case "$1" in+*.pdf)+    if [ -s "$1" ]; then+        exec pdftotext "$1" -+    else+        exec cat+    fi+    ;;+*)+    exec cat+    ;;+esac

bin/psql-kill

new file mode 100755@@ -0,0 +1,15 @@+#!/bin/sh++set -e++if [ $# != 1 ]; then+  echo "Usage: $(basename $0) db-name" >&2+  exit 1+fi++dbname="$1"+psql "$dbname" -c '+  SELECT pg_terminate_backend(pg_stat_activity.pid)+  FROM pg_stat_activity+  WHERE datname = '"'$dbname'"' AND pid != pg_backend_pid();+'

bin/random-text

new file mode 100755@@ -0,0 +1,24 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function++import argparse+import random++LETTERS = map(chr, range(ord('a'), ord('z') + 1))+++if __name__ == '__main__':+    p = argparse.ArgumentParser()+    p.add_argument('size', type=int)+    p.add_argument('--punct', default='\n')+    p.add_argument('--punct-ratio', type=float, default=0.05)+    args = p.parse_args()++    data = []+    for _ in xrange(args.size):+        if random.random() < args.punct_ratio:+            data.append(random.choice(args.punct))+        else:+            data.append(random.choice(LETTERS))+    print(''.join(data))

bin/rbt

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++RUST_BACKTRACE=1 "$@"

bin/remote-old-sync

new file mode 100755Binary file changed

bin/ren

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec nvim -c Renamer

bin/reset-term

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/bash++echo -ne "\033[0m"

bin/restart-xcompmgr

new file mode 100755@@ -0,0 +1,26 @@+#!/bin/sh++# See: https://github.com/chjj/compton/wiki/vsync-guide++killall xcompmgr --wait+exec xcompmgr++# killall compton --wait+# exec compton --backend glx --vsync+# exec compton --backend glx+# exec compton --backend glx \+    # --glx-no-stencil \+    # --glx-no-rebind-pixmap \+    # --use-damage \+# allow_rgb10_configs=false exec compton \+  # --backend glx \+  # --glx-no-stencil \+  # --glx-no-rebind-pixmap++# exec compton \+  # --backend xr_glx_hybrid \+  # --paint-on-overlay \+  # --glx-no-stencil \+  # --glx-no-rebind-pixmap++# exec compton --paint-on-overlay

bin/rge

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec nvim $(rg -l "$@")

bin/rgp

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec rg -p "$@" | less -RFX

bin/rscp

new file mode 100755@@ -0,0 +1,10 @@+#!/bin/sh++rsync \+    --archive \+    --recursive \+    --human-readable \+    --partial \+    --inplace \+    --progress \+    "$@"

bin/rscp-android

new file mode 100755@@ -0,0 +1,10 @@+#!/bin/sh++rsync \+    --recursive \+    --human-readable \+    --partial \+    --inplace \+    --progress \+    --size-only \+    "$@"

bin/run-at-int

new file mode 100755@@ -0,0 +1,20 @@+#!/bin/sh++case $# in+  0|1|2) echo "Usage: $(basename $0) NUMBER[SUFFIX] UNTIL command ..." >&2 ; exit 1 ;;+esac++sleepTime="$1"+shift++until=0+[ "$1" != "0" ] && until=$(date --date "$1" +%s)+shift++while true; do+  if [ "$until" != "0" ] && [ $(date +%s) -ge "$until" ]; then+    exit+  fi+  "$@"+  sleep $sleepTime+done

bin/run-wingo

new file mode 100755@@ -0,0 +1,14 @@+#!/bin/sh++. "$HOME/bin/x11-go"++logdir="$HOME/data/log"+logfile="$logdir/wingo.log"+mkdir -p "$logdir"++# shellcheck disable=SC2129+echo "======================================================" >> $logfile+echo "======================================================" >> $logfile+echo "======================================================" >> $logfile+#wingo --config-dir $HOME/data/projects/wingo/config --log-level 3 --log-colors -p 1 $* > $logfile 2>&1+wingo -p 4 --log-level 3 --log-colors "$@" >> $logfile 2>&1

bin/runon

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++host="$1"+shift+if [ -z "$host" ]; then+    echo "Usage: $(basename "$0") <host> <command> ..." >&2+    exit 1+fi+exec ssh "$host" -tt "zsh -i -c '$*'" < /dev/null

bin/runon-all

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++all-list | while read -r host; do+    echo "===== $host ====="+    runon "$host" "$@"+done

bin/rust-analyzer-wrapper

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++bin="$CARGO_INSTALL_ROOT/bin/rust-analyzer"+if [ -n "$RA_LOG_STDERR" ]; then+  exec "$bin" 2> "$RA_LOG_STDERR"+else+  exec "$bin"+fi

bin/rustenv

new file mode 100644@@ -0,0 +1,8 @@+#!/bin/sh++kind=release+if [ -n "$1" ]; then+  kind="$1"+fi+PATH="$(pwd)/target/$kind:$PATH"+export PATH

bin/s3-login

new file mode 100755@@ -0,0 +1,12 @@+#!/bin/sh++if [ $# != 2 ]; then+  echo "Usage: source <(s3-login access-key-file secret-key-file)" >&2+  exit 1+fi++access="$1"+secret="$2"++echo "export AWS_ACCESS_KEY_ID=\"$(cat $access)\""+echo "export AWS_SECRET_ACCESS_KEY=\"$(cat $secret)\""

bin/screenshot

new file mode 100755@@ -0,0 +1,11 @@+#!/bin/sh++if [ -z "$1" ]; then+    echo "Usage: $(basename $0) <file>" >&2+    exit 1+fi++file="$1"+sleep 3+echo "select a window!"+exec scrot -s "$file"

bin/sendmail

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec msmtp "$@"

bin/setbright

new file mode 100755@@ -0,0 +1,12 @@+#!/bin/bash++case "$(hostname)" in+  Tiger|Turtle|monty|krusty)+    path="/sys/class/backlight/intel_backlight/brightness"+    echo -n $1 | sudo tee "$path" > /dev/null+    ;;+  *)+    echo "Uncrecognized latop $(hostname)." >&2+    exit 1+    ;;+esac

bin/settime

new file mode 100755@@ -0,0 +1,4 @@+#!/bin/bash++ntpdate pool.ntp.org+hwclock --systohc

bin/setup-home

new file mode 100755@@ -0,0 +1,62 @@+#!/bin/sh++set -e++# Test that we can SSH to the home server OK.+# if ! ssh -p 7704 -o StrictHostKeyChecking=no andrew@home.burntsushi.net echo 'calling home successful'; then+  # echo 'calling home failed' >&2+  # exit 1+# fi++if [ ! -f "$HOME/.zprofile" ]; then+  trash=".bashrc .bash_logout .bash_profile .profile .vimrc .ssh/config"+  mkdir -p .old+  for f in $trash; do+    if [ -f "$f" ]; then+      mv "$f" .old/+    fi+  done+fi++if [ ! -d "$HOME/.git" ]; then+  git init+  git remote add origin 'https://github.com/BurntSushi/dotfiles'+  git pull origin master --ff-only+  git branch --set-upstream-to origin/master master+  chmod 600 "$HOME/.ssh/config"+else+  git pull origin master --ff-only+fi++# Update path to include new ~/bin checkout.+export PATH="$HOME/bin:$PATH"++# Configure $HOME and $HOME/bin git checkouts.+(cd "$HOME" && configure-git)++# Install ripgrep+if ! cmd-exists rg; then+  CARGO_BIN="$HOME/.local/cargo/bin"+  mkdir -p "$CARGO_BIN"+  vers="$(+      curl -s https://api.github.com/repos/BurntSushi/ripgrep/releases/latest \+          | grep '"tag_name":' \+          | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+'+  )"+  url="https://github.com/BurntSushi/ripgrep/releases/download/$vers/ripgrep-$vers-x86_64-unknown-linux-musl.tar.gz"+  curl -sL "$url" > "/tmp/ripgrep-$vers.tar.gz"+  tar xf "/tmp/ripgrep-$vers.tar.gz" -C /tmp+  cp "/tmp/ripgrep-$vers-x86_64-unknown-linux-musl/rg" "$CARGO_BIN/rg"+fi++# An Ubuntu box means this is probably just a temporary remote box, so+# install some nice things.+if cmd-exists apt; then+  neovim-install-appimage+  sudo apt update+  for p in jq htop iotop zsh; do+    if ! cmd-exists "$p"; then+      sudo apt install -y "$p"+    fi+  done+fi

bin/sort-filesize

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/bash++if [ $# = 0 ]; then+  dir=.+else+  dir=("$@")+fi+du -achx -d 1 "${dir[@]}" | sort -h

bin/src-color-pdf

new file mode 100755@@ -0,0 +1,47 @@+#!/bin/bash++customdir="--data-dir=$HOME/.config/highlight/"+custom=+srctype=+# font="Hack Nerd Font"+# font="Hack Regular Nerd Font Complete Mono"+# font="UbuntuMono-R"+size=15+theme=bclear+linens=++highlightcss=+[[ -f highlight.css ]] && highlightcss=1++function usage {+  echo "Usage: $(basename $0): [-c] [-l] [-f font] [-s font-size] [-t theme] input-file output-file" >&2+  exit 1+}++while [ $# -gt 0 ]; do+  case $1 in+  -c)    custom=$customdir ; shift ;;+  # -f)    font=$2 ; shift 2 ;;+  -s)    size=$2 ; shift 2 ;;+  -S)    srctype="-S $2" ; shift 2 ;;+  -t)    theme=$2 ; shift 2 ;;+  -l)    linens='-l' ; shift ;;+  -h|-*) usage ;;+   *)    break ;;+  esac+done++if [ ! $# -eq 2 ]; then+  usage+fi++ifile=$1+ofile=$2++highlight -i $ifile $srctype -s $theme --inline-css -K $size -t 4 \+          $custom $linens \+  | wkhtmltopdf - $ofile++if [ -z "$highlightcss" ] && [ -f highlight.css ]; then+  rm highlight.css+fi

bin/ssh-add-key

new file mode 100755@@ -0,0 +1,29 @@+#!/bin/sh++set -e++name="$1"+org="$2"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <ssh-key-name> [<org-name>]" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi++key="$HOME/.ssh/keys/$name"+if [ "$name" = "google-compute-engine" ]; then+  key="$HOME/.ssh/google_compute_engine"+fi+if ! [ -f "$key" ]; then+    echo "$key: does not exist" >&2+    exit 1+fi++passphrase="$org/ssh/$name/passphrase"+if ! pass "$passphrase" > /dev/null 2>&1; then+    ssh-add "$key"+else+    ssh-add-pass-key "$key" "$passphrase"+fi

bin/ssh-add-keys

new file mode 100755Binary file changed

bin/ssh-add-pass-key

new file mode 100755@@ -0,0 +1,19 @@+#!/bin/sh++set -e++if [ $# != 2 ]; then+    echo "Usage: $(basename "$0") ssh-private-key-path pass-key-path" >&2+    exit 1+fi++sshkey="$1"+passkey="$2"+runner="$(ssh-gen-askpass-runner "$passkey")"+display=dummy+if [ -n "$DISPLAY" ]; then+    display="$DISPLAY"+fi++SSH_ASKPASS="$runner" DISPLAY="$display" ssh-add "$sshkey" < /dev/null+rm "$runner"

bin/ssh-create-key

new file mode 100755@@ -0,0 +1,19 @@+#!/bin/sh++set -e++name="$1"+org="$2"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <ssh-key-name> [<org-name>]" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi++mkdir -p "$HOME/.ssh/keys"+pass generate -n -c "$org/ssh/$name/passphrase"+ssh-keygen -f "$HOME/.ssh/keys/$name" -C "$name"+pass insert --multiline "$org/ssh/$name/pub" < "$HOME/.ssh/keys/$name.pub"+pass insert --multiline "$org/ssh/$name/priv" < "$HOME/.ssh/keys/$name"

bin/ssh-create-unencrypted-key

new file mode 100755@@ -0,0 +1,27 @@+#!/bin/sh++# This should be used sparingly, but is useful in certain circumstances.+#+# For example, this is used in my media PCs which want to be able to connect+# to the local NAS without dealing with passwords.+#+# I also use this to create ssh keys to store on phones. In that case, the+# key should only be used on that phone. In the case where a phone is lost or+# compromised, one can revoke that key.++set -e++name="$1"+org="$2"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <ssh-key-name> [<org-name>]" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi++mkdir -p "$HOME/.ssh/keys"+ssh-keygen -f "$HOME/.ssh/keys/$name" -C "$name"+pass insert --multiline "$org/ssh/$name/pub" < "$HOME/.ssh/keys/$name.pub"+pass insert --multiline "$org/ssh/$name/priv" < "$HOME/.ssh/keys/$name"

bin/ssh-delete-key

new file mode 100755@@ -0,0 +1,16 @@+#!/bin/sh++set -e++name="$1"+org="$2"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <ssh-key-name>" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi++rm -f "$HOME/.ssh/keys/$name.pub"+rm -f "$HOME/.ssh/keys/$name"

bin/ssh-extract-key

new file mode 100755@@ -0,0 +1,26 @@+#!/bin/sh++set -e++name="$1"+org="$2"+dir="$3"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <ssh-key-name>" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi+if [ -z "$dir" ]; then+    dir="$HOME/.ssh/keys"+fi++mkdir -p "$dir"+# Some weird keys do not have a public key.+if pass "$org/ssh/$name/pub" >/dev/null 2>&1; then+    pass "$org/ssh/$name/pub" > "$dir/$name.pub"+    chmod 0600 "$dir/$name.pub"+fi+pass "$org/ssh/$name/priv" > "$dir/$name"+chmod 0600 "$dir/$name"

bin/ssh-gen-askpass-runner

new file mode 100755@@ -0,0 +1,18 @@+#!/bin/sh++set -e++if [ $# != 1 ]; then+    echo "Usage: $(basename "$0") pass-key-path" >&2+    exit 1+fi++key="$1"+tmpfile="$(mktemp -t ssh-askpass-runner-XXXXX.sh)"+cat > "$tmpfile" <<EOF+#!/bin/sh++pass "$key" | head -n1+EOF+chmod +x "$tmpfile"+echo "$tmpfile"

bin/ssh-send-key

new file mode 100755@@ -0,0 +1,24 @@+#!/bin/sh++set -e++host="$1"+name="$2"+org="$3"+if [ -z "$host" ]; then+    echo "Usage: $(basename "$0") <host> <ssh-key-name> [<org-name>]" >&2+    exit 1+fi+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <host> <ssh-key-name> [<org-name>]" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi++trap 'rm -f /tmp/ssh-send-key/$name*' 0+mkdir -p /tmp/ssh-send-key+pass "$org/ssh/$name/pub" > "/tmp/ssh-send-key/$name.pub"+pass "$org/ssh/$name/priv" > "/tmp/ssh-send-key/$name"+ssh-copy-id -i "/tmp/ssh-send-key/$name.pub" "$host"

bin/ssh-set-default

new file mode 100755@@ -0,0 +1,17 @@+#!/bin/sh++set -e++name="$1"+org="$2"+if [ -z "$name" ]; then+    echo "Usage: $(basename "$0") <ssh-key-name>" >&2+    exit 1+fi+if [ -z "$org" ]; then+    org=andrew+fi++ssh-extract-key "$name" "$org"+ln -s "keys/$name.pub" "$HOME/.ssh/id_rsa.pub"+ln -s "keys/$name" "$HOME/.ssh/id_rsa"

bin/ssh-setup-home

new file mode 100755@@ -0,0 +1,13 @@+#!/bin/sh++set -e++if [ $# = 0 ]; then+    echo "Usage: $(basename $0) <host> ..." >&2+    exit 1+fi+for host; do+    echo "=== $host === "+    scp -o StrictHostKeyChecking=no "$HOME/.ssh/external" "$host:.ssh/id_rsa"+    ssh "$host" 'curl -s https://burntsushi.net/stuff/setup-home | sh -l'+done

bin/standby

new file mode 100755@@ -0,0 +1,4 @@+#!/bin/sh++sleep 1+xset dpms force standby

bin/stuffit

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++rscp "$@" bs:~/www/burntsushi.net/stuff/

bin/sum-lines

new file mode 100755@@ -0,0 +1,6 @@+#!/usr/bin/env python2++from __future__ import absolute_import, division, print_function+import sys++print(sum(float(line.strip()) for line in sys.stdin))

bin/system76-darter-configure-touchpad

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++# `xinput list` shows devices.+# `xinput list-props <device>` shows properties.+device="SynPS/2 Synaptics TouchPad"+# enable tap-to-click+xinput set-prop "$device" "libinput Tapping Enabled" 1+# make the touchpad a bit more sensitive+xinput set-prop "$device" "libinput Accel Speed" 0.45

bin/system76-darter-hook-sleep

new file mode 100755@@ -0,0 +1,23 @@+#!/bin/sh++# This is a systemd hook script that is run whenever+# suspend/resume takes place. It should be symlinked into+# /usr/lib/systemd/system-sleep.++# To see logs from this script, use:+#+#   journalctl -b -u systemd-suspend.service++# $1 is 'pre' (going to sleep) or 'post' (waking up)+# $2 is 'suspend', 'hibernate' or 'hybrid-sleep'+case "$1/$2" in+  pre/*)+    if lsmod | grep -q thunderbolt; then+      sudo rmmod thunderbolt+    fi+    ;;+  post/*)+    /home/andrew/bin/system76-darter-set-kbd-backlight+    sudo modprobe thunderbolt+    ;;+esac

bin/system76-darter-set-kbd-backlight

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++dir="/sys/class/leds/system76_acpi::kbd_backlight"+# sudo sh -c "echo FFA500 > $dir/color"+sudo sh -c "echo FF7F00 > $dir/color"+sudo sh -c "echo 255 > $dir/brightness"

bin/tarfiles

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++d="$1"+shift++mkdir "$d"+cp -a "$@" "$d"/+tar zcf "$d.tar.gz" "$d"

bin/terminal

new file mode 100755@@ -0,0 +1,72 @@+#!/bin/sh++# vim: tabstop=2 shiftwidth=2 softtabstop=2++. $HOME/bin/x11-go++case "$(hostname)" in+  Tiger)+    if [ "$(gohead tabs | cut -f2)" = "vga" ]; then+      conf="$HOME/.config/alacritty/tiger-big.yml"+    else+      conf="$HOME/.config/alacritty/tiger.yml"+    fi+    temubin="alacritty --config-file $conf"+    temubin="$temubin --dimensions 80 30"+    ;;+  monty)+    if [ "$(gohead tabs | cut -f2)" = "vga" ]; then+      conf="$HOME/.config/alacritty/tiger-big.yml"+    else+      conf="$HOME/.config/alacritty/tiger.yml"+    fi+    temubin="alacritty --config-file $conf"+    temubin="$temubin --dimensions 80 30"+    ;;+  krusty)+    if [ "$(gohead tabs | cut -f2)" = "vga" ]; then+      conf="$HOME/.config/alacritty/tiger-big.yml"+    else+      conf="$HOME/.config/alacritty/tiger.yml"+    fi+    temubin="alacritty --config-file $conf"+    temubin="$temubin --dimensions 80 30"+    ;;+  Jaguar)+    conf="$HOME/.config/alacritty/jaguar.yml"+    temubin="alacritty --config-file $conf"+    ;;+  Leopard)+    conf="$HOME/.config/alacritty/jaguar.yml"+    temubin="alacritty --config-file $conf"+    ;;+  wiggum)+    conf="$HOME/.config/alacritty/jaguar.yml"+    temubin="alacritty --config-file $conf"+    ;;+  *)+    if is-work; then+      if is-laptop; then+        conf="$HOME/.config/alacritty/work-laptop.yml"+        temubin="alacritty --config-file $conf"+      else+        conf="$HOME/.config/alacritty/work-desktop.yml"+        temubin="alacritty --config-file $conf"+      fi+    else+      temubin="alacritty"+    fi+    ;;+esac++dir=+if [ "$1" = "-d" ]; then+  dir="$2"+  echo "$dir" >> /tmp/terminal.log+  shift 2+fi+if [ -z "$dir" ]; then+  exec $temubin "$@" >> /tmp/alacritty.log 2>&1+else+  exec $temubin --working-directory "$dir" "$@" >> /tmp/alacritty.log 2>&1+fi

bin/terminal-cwd

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++if command -v alacritty > /dev/null 2>&1; then+  exec alacritty --working-directory "$PWD" "$@"+else+  exec konsole --workdir "$PWD" "$@"+fi+

bin/terminal-tmux-remote

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++if [ -z "$1" ]; then+    echo "Usage: $(basename $0) <host>" >&2+fi+host="$1"++exec terminal -e ssh -A "$host" -t tmux -u new -A -s remote

bin/text2pdf

new file mode 100755@@ -0,0 +1,16 @@+#!/bin/sh++first=0+for f; do+  if [ $first != 0 ]; then+    echo '--------------------------------------------------------------------------------'+    first=1+  fi++  echo "File: " $f+  echo+  cat $f+done | enscript --font="DejaVuSansMono11" --media Letter \+         --margins=25:25:: \+         --header='$n|Page $% of $=' -o - | ps2pdf - -+

bin/tmux-cleanup

new file mode 100755@@ -0,0 +1,11 @@+#!/bin/sh++unattached_local=$(+  tmux ls \+    | grep -E '^[0-9]+:' \+    | grep -E -v '\(attached\)' \+    | cut -d':' -f1+)+for session in $unattached_local; do+  tmux kill-session -t $session+done

bin/tmux-renumber-sessions

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/bash++new=1+for old in $(tmux ls | grep -E '^[0-9]+:' | cut -f1 -d':' | sort -n); do+  tmux rename-session -t "$old" "$new"+  ((new++))+done

bin/toggle-tap-to-click

new file mode 100755@@ -0,0 +1,31 @@+#!/bin/sh++if [ ! -f "$HOME/.laptop" ]; then+  exit 1+fi+case "$(hostname)" in+  krusty)+    device="SynPS/2 Synaptics TouchPad"+    prop="libinput Tapping Enabled"+    ;;+  *)+    echo "unrecognize machine: $(hostname)" >&2+    ;;+esac++current="$(+  xinput list-props "$device" | rg "${prop}[\s()0-9]+:\s+([0-9]+)" -or '$1'+)"+if [ "$current" = "0" ]; then+  xinput set-prop "$device" "$prop" 1++  icon=input-touchpad-symbolic.symbolic+  hint=string:synchronous:taptoclick+  notify-send -h "$hint" -i "$icon" -t 1500 "tap-to-click enabled"+else+  xinput set-prop "$device" "$prop" 0++  icon=touchpad-disabled-symbolic.symbolic+  hint=string:synchronous:taptoclick+  notify-send -h "$hint" -i "$icon" -t 1500 "tap-to-click disabled"+fi

bin/ubuntu-gnome-terminal

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++"$HOME/bin/terminal" -e tmux -u new zsh

bin/ubuntu-install-ripgrep

new file mode 100755@@ -0,0 +1,17 @@+#!/bin/sh++curvers=+if command -V rg > /dev/null 2>&1; then+    curvers="$(rg -V | cut -d' ' -f2)"+fi+latesturl="https://api.github.com/repos/BurntSushi/ripgrep/releases/latest"+latestvers="$(curl -s "$latesturl" | jq -r .tag_name)"+if [ "$curvers" = "$latestvers" ]; then+    echo "ripgrep is up to date"+    exit 0+fi++name="ripgrep_${latestvers}_amd64.deb"+url="https://github.com/BurntSushi/ripgrep/releases/download/$latestvers/$name"+(cd /tmp && curl -LO "$url" && sudo dpkg -i "$name")+echo "ripgrep updated from $curvers to $latestvers"

bin/ubuntu-upgrade

new file mode 100755@@ -0,0 +1,5 @@+#!/bin/sh++/usr/bin/sudo apt-get update+/usr/bin/sudo apt-get --with-new-pkgs upgrade+/usr/bin/sudo apt autoremove

bin/urldecode-copy

new file mode 100755@@ -0,0 +1,5 @@+#!/bin/sh++x=$(python2 -c "import urllib; print urllib.unquote('$1'),")+echo $x+printf "%s" $x | xclip -selection clipboard

bin/urlencode-copy

new file mode 100755@@ -0,0 +1,6 @@+#!/bin/sh++# RFC 3986, please.+x=$(python2 -c "import urllib; print urllib.quote('$1', safe='~'),")+echo $x+printf "%s" $x | xclip -selection clipboard

bin/utf8-to-utf16

new file mode 100755@@ -0,0 +1,7 @@+#!/bin/sh++path="$1"+tmppath=$(mktemp)+cp "$path" "$tmppath"+iconv -f UTF-8 -t UTF-16 "$tmppath" > "$path"+rm "$tmppath"

bin/v

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/bash++exec nvim "$@"+# if [ -f Cargo.toml ]; then+#   exec vim -S $HOME/.vim/session-rust.vim "$@"+# else+#   exec vim "$@"+# fi

bin/video

new file mode 100755@@ -0,0 +1,122 @@+#!/bin/bash++. $HOME/bin/x11-go++function headNum {+  headName="$1"+  gohead tabs | cut -f1-2 | grep "$headName" | cut -f1+}++function placeHead {+  headName="$1"+  geom=$(gohead query "$headName" | cut -f2-5)+  x=$(echo "$geom" | cut -f1)+  y=$(echo "$geom" | cut -f2)+  w=$(echo "$geom" | cut -f3)+  h=$(echo "$geom" | cut -f4)++  place $(headNum "$headName") $x $y $w $h+}++function array_exists {+  search="$1"+  haystack=($2)++  for needle in ${haystack[@]}; do+    if [ "$search" == "$needle" ]; then+      return 0+    fi+  done++  return 1+}++function array_key {+  search="$1"+  keys=($2)+  values=($3)++  for (( i=0; i<${#values[*]}; i++ )); do+    if [ "$search" == "${values[$i]}" ]; then+      echo "${keys[$i]}"+    fi+  done+}++if ! gohead set $*; then+  exit 1+fi+sleep 1++case `hostname` in+frink)+  ps aux | grep -v grep | grep dzen-workspaces | awk '{print $2}' | xargs -r kill+  killall trayer+  (sleep 1 && \+    trayer \+    --widthtype pixel --width 150 \+    --heighttype pixel --height 40 \+    --edge bottom --align right --monitor 1 \+    --SetDockType true) &+  (sleep 1 && \+    $HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \+    | dzen2 -dock -xs 2 -h 40 -fn 'Cousine-18' -ta l -bg '#ffffff' -y 1170) &+  ;;+Tiger)+  if array_exists laptop "$*"; then+    placeHead laptop+  else+    placeHead "$1"+  fi+  autobright &+  ;;+monty)+  if array_exists laptop "$*"; then+    placeHead laptop+  else+    placeHead "$1"+  fi+  autobright &+  ;;+krusty)+  if array_exists laptop "$*"; then+    placeHead laptop+  else+    placeHead "$1"+  fi+  autobright &+  ;;+Turtle)+  if array_exists laptop "$*"; then+    placeHead laptop+  else+    placeHead "$1"+  fi+  autobright &+  ;;+Leopard)+  ;;+Jaguar)+  ;;+wiggum)+  pgrep dzen-workspaces | xargs -r kill+  killall trayer+  (sleep 1 && \+    trayer \+    --widthtype pixel --width 150 \+    --heighttype pixel --height 40 \+    --edge bottom --align right \+    --SetDockType true) &++  headHeight="$(gohead query tv | cut -f4)"+  y="$((headHeight - 40))"+  (sleep 1 && \+    $HOME/.config/wingo/scripts/dzen-workspaces/dzen-workspaces \+    | dzen2 -dock -xs 2 -h 40 -fn 'Cousine-18' -ta l -bg '#ffffff' -y "$y") &+  ;;+*)+  echo "Unrecognized machine '$(hostname)'." >&2+  exit 1+  ;;+esac+

bin/video-scale

new file mode 100755@@ -0,0 +1,18 @@+#!/bin/sh++vertical="$1"+input="$2"+output="$3"+if [ -z "$vertical" ] || [ -z "$input" ]; then+  echo "Usage: $(basename "$0") <vertical-resolution> <input> [<output>]" >&2+  exit 1+fi+if [ -z "$output" ]; then+  d="$(dirname "$input")"+  f="$(basename "$input")"+  ext="${f##*.}"+  noext="${f%.*}"+  output="$d/${noext}_${vertical}p.${ext}"+fi+exec ffmpeg -y \+  -i "$input" -vf "scale=-1:$vertical" -c:v libx264 -c:a copy "$output"

bin/video-slice

new file mode 100755@@ -0,0 +1,45 @@+#!/bin/sh++usage() {+  echo "Usage: $(basename "$0") [ --accurate ] <start> <end> <input> [<output>]"+}++accurate=+while true; do+  case "$1" in+    --accurate) accurate=1 && shift ;;+    -h|--help)+      usage+      exit 0+      ;;+    -*)+      usage >&2+      exit 1+      ;;+    *) break ;;+  esac+done++start="$1"+end="$2"+input="$3"+output="$4"+if [ -z "$start" ] || [ -z "$end" ] || [ -z "$input" ]; then+  usage >&2+  exit 1+fi+if [ -z "$output" ]; then+  d="$(dirname "$input")"+  f="$(basename "$input")"+  ext="${f##*.}"+  noext="${f%.*}"+  output="$d/${noext}_${start}-${end}.${ext}"+fi++# See: https://stackoverflow.com/a/52916510/619216+# See: https://news.ycombinator.com/item?id=22777179+if [ -n "$accurate" ]; then+  exec ffmpeg -ss "$start" -to "$end" -i "$input" "$output"+else+  exec ffmpeg -ss "$start" -to "$end" -i "$input" -c copy -map 0 "$output"+fi

bin/video-to-720p

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++video-scale 720 "$@"

bin/video-to-gif

new file mode 100755@@ -0,0 +1,9 @@+#!/bin/sh++if [ $# != 2 ]; then+    echo "Usage: $(basename "$0") <input-video-file> <output-gif-file>" >&2+    exit 1+fi+src="$1"+dst="$2"+exec ffmpeg -i "$src" -r 10 -vf scale=200:-1 "$dst"

bin/vless

new file mode 100755@@ -0,0 +1,19 @@+#!/bin/sh++# less="$VIMRUNTIME/macros/less.sh"+# args="-s ~/.vimrc.less "+# args="-c \":source /home/andrew/.vimrc.less\""+# args="$args -c \":au SwapExists * let v:swapchoice='0'\""+# args="$args -n"+# args="$args -R"+# args="$args -c \":set cmdheight=2\""++# File descriptor 1 is standard output+if [ -t 1 ]; then+  if [ $# -eq 0 ]; then+    set -- -+  fi+  # $less $args $@+  vim -s ~/.vimrc.less "$@"+  # $VIMRUNTIME/macros/less.sh -s ~/.vimrc.less $@+fi

bin/volume

new file mode 100755@@ -0,0 +1,61 @@+#!/bin/sh++case "$(hostname)" in+  # frink)+    # sink="alsa_output.pci-0000_00_1b.0.analog-stereo"+    # case $1 in+      # up) pactl set-sink-mute "$sink" false && pactl set-sink-volume "$sink" +2% ;;+      # down) pactl set-sink-mute "$sink" false && pactl set-sink-volume "$sink" -2% ;;+      # mute) pactl set-sink-mute "$sink" toggle ;;+    # esac+    # ;;+  Jaguar)+    sink="alsa_output.pci-0000_00_1f.3.hdmi-stereo"+    case $1 in+      up) pactl set-sink-mute "$sink" false && pactl set-sink-volume "$sink" +2% ;;+      down) pactl set-sink-mute "$sink" false && pactl set-sink-volume "$sink" -2% ;;+      mute) pactl set-sink-mute "$sink" toggle ;;+    esac+    ;;+  wiggum)+    sink="alsa_output.pci-0000_00_1f.3.hdmi-stereo"+    case $1 in+      up) pactl set-sink-mute "$sink" false && pactl set-sink-volume "$sink" +2% ;;+      down) pactl set-sink-mute "$sink" false && pactl set-sink-volume "$sink" -2% ;;+      mute) pactl set-sink-mute "$sink" toggle ;;+    esac+    ;;+  frink)+    case $1 in+      up)   i3-volume -p -n -i 10 ;;+      down) i3-volume -p -n -d 10 ;;+      mute) i3-volume -p -n -m ;;+    esac+    ;;+  krusty)+    case $1 in+      up)   i3-volume -p -n -i 10 ;;+      down) i3-volume -p -n -d 10 ;;+      mute) i3-volume -p -n -m ;;+    esac+    ;;+  *)+    case "$(hostname)" in+      *)      control='Master' mcontrol='Master' ;;+    esac+    case "$(hostname)" in+      Jaguar|frink|monty|Turtle|wiggum|krusty)+        card=0+        ;;+      *)+        card=1+        ;;+    esac++    case $1 in+      up)   amixer -c$card set "$control" 2+ unmute ;;+      down) amixer -c$card set "$control" 2- unmute ;;+      mute) amixer -q set "$mcontrol" toggle ;;+    esac+    ;;+esac

bin/x11-gnome-do

new file mode 100755@@ -0,0 +1,423 @@+#!/usr/bin/env python3++# This script serves as duct tape to make the multiple monitor experience+# on GNOME 3 just a little bit better. Currently, the only functionality of+# this script is to switch focus from one monitor to the next, while respecting+# the stacking order of windows inside each individual monitor. However, the+# script is designed to make it easy to add more functionality later. Namely,+# when the script is run, it reads the current monitor, desktop and window+# configuration into a convenient in-memory data structure.+#+# This program is made simply by relying on some external commands:+#+#   wmctrl+#   xprop+#   gohead (if not available, then xrandr is used)+#+# gohead can be installed from: https://github.com/BurntSushi/gohead+# The only advantage of gohead is that it can be used to give "nice" names+# to your monitors. For example, on my laptop, I have this:+#+#     $ cat ~/.config/gohead/config.ini+#     [Monitors]+#     laptop = DP-0+#     left = DP-2+#     right = HDMI-0+#+# You can then run `x11-gnome-do focus-monitor left`. If your monitor+# configuration ever changes, then you just need to update the gohead+# mapping.++import argparse+import os+import re+from collections import OrderedDict, namedtuple+import subprocess+++class Monitor(namedtuple('Monitor', [+    'index',+    'nice_name',+    'output_name',+    'x',+    'y',+    'width',+    'height',+    'primary',+])):+    @staticmethod+    def parse_gohead(gohead_line):+        parser = re.compile(r'''(?xm)+            ^+            (?P<index>[0-9]+)+            \s++            (?P<nice>\S+)+            \s++            (?P<output>\S+)+            \s++            (?P<x>[0-9]+)+            \s++            (?P<y>[0-9]+)+            \s++            (?P<width>[0-9]+)+            \s++            (?P<height>[0-9]+)+            \s*+            (?P<primary>primary)?+            \s*+            $+        ''')+        m = parser.match(gohead_line)+        if m is None:+            raise Exception(f'invalid gohead monitor line: "{gohead_line}"')+        return Monitor(+            index=int(m.group('index')),+            nice_name=m.group('nice'),+            output_name=m.group('output'),+            x=int(m.group('x')),+            y=int(m.group('y')),+            width=int(m.group('width')),+            height=int(m.group('height')),+            primary=m.group('primary') == 'primary',+        )++    @staticmethod+    def parse_xrandr(index, xrandr_line):+        '''+        Parses a monitor configuration from a line outputted by xrandr.+        The line must start with "OUTPUT-NAME connected".+        '''+        parser = re.compile(r'''(?xm)+            ^+            (?P<output>\S+)+            \s++            connected+            \s++            (?P<primary>primary)?\s*+            (?P<width>[0-9]+)x(?P<height>[0-9]+)+            \+(?P<x>[0-9]+)\+(?P<y>[0-9]+)+            \s++        ''')+        m = parser.match(xrandr_line)+        if m is None:+            raise Exception(f'invalid xrandr monitor line: "{xrandr_line}"')+        return Monitor(+            index=index,+            nice_name=m.group('output'),+            output_name=m.group('output'),+            x=int(m.group('x')),+            y=int(m.group('y')),+            width=int(m.group('width')),+            height=int(m.group('height')),+            primary=m.group('primary') == 'primary',+        )++    def contains(self, win):+        # For now, we just check which monitor contains the top left corner of+        # the window, even if the window has more overlap with a different+        # monitor.+        return (+            win.x >= self.x+            and win.y >= self.y+            and win.x < (self.x + self.width)+            and win.y < (self.y + self.height)+        )++    def is_name_match(self, name):+        name = name.lower()+        return name in [self.nice_name.lower(), self.output_name.lower()]+++class Desktop(namedtuple('Desktop', [+    'index',+    'name',+    'current',+    'width',+    'height',+    'work_x',+    'work_y',+    'work_width',+    'work_height',+    'windows',+])):+    @staticmethod+    def parse(wmctrl_line):+        parser = re.compile(r'''(?xm)+            ^+            (?P<index>[0-9]+)+            \s++            (?P<current>-|\*)+            \s++            DG:\s+(?P<width>[0-9]+)x(?P<height>[0-9]+)+            \s++            VP:\s+\S++            \s++            WA:\s+(?P<wx>[0-9]+),(?P<wy>[0-9]+)+               \s+(?P<wwidth>[0-9]+)x(?P<wheight>[0-9]+)+            \s*+            (?P<name>.*)+            \s*+            $+        ''')+        m = parser.match(wmctrl_line)+        if m is None:+            raise Exception(f'invalid wmctrl desktop line: "{wmctrl_line}"')+        return Desktop(+            index=int(m.group('index')),+            name=m.group('name') or None,+            current=m.group('current') == '*',+            width=int(m.group('width')),+            height=int(m.group('height')),+            work_x=int(m.group('wx')),+            work_y=int(m.group('wy')),+            work_width=int(m.group('wwidth')),+            work_height=int(m.group('wheight')),+            windows=[],+        )+++class Window(namedtuple('Window', [+    'id',+    'name',+    'monitor',+    'desktop_index',+    'desktop',+    'x',+    'y',+    'width',+    'height',+    'client_name',+])):+    @staticmethod+    def parse(wmctrl_line):+        parser = re.compile(r'''(?xm)+            ^+            (?P<id>0x[0-9a-fA-F]+)+            \s++            (?P<desk>[0-9]+)+            \s++            (?P<x>[0-9]+)+            \s++            (?P<y>[0-9]+)+            \s++            (?P<width>[0-9]+)+            \s++            (?P<height>[0-9]+)+            \s++            (?P<client>\S+)+            \s*+            (?P<name>.*)+            \s*+            $+        ''')+        m = parser.match(wmctrl_line)+        if m is None:+            raise Exception(f'invalid wmctrl window line: "{wmctrl_line}"')+        return Window(+            id=int(m.group('id'), base=16),+            name=m.group('name'),+            monitor=None,+            desktop_index=int(m.group('desk')),+            desktop=None,+            x=int(m.group('x')),+            y=int(m.group('y')),+            width=int(m.group('width')),+            height=int(m.group('height')),+            client_name=m.group('client'),+        )++    def is_sticky(self):+        return self.desktop_index == -1+++def read_monitors():+    '''+    Read the existing monitor configuration. This only includes+    monitors that are both connected and active.++    This attempts to use gohead first, which permits associating "nice"+    names with monitors. If gohead isn't installed, then xrandr is+    used.++    The dictionary returned is keyed by the monitor's "nice" name. (Or+    just output name if gohead isn't available.)+    '''+    gohead = os.path.join(os.getenv('HOME'), '.gox11', 'bin', 'gohead')+    if not os.path.exists(gohead):+        return read_monitors_xrandr()++    out = subprocess.run(+        [gohead, 'tabs'],+        encoding='utf-8',+        stdout=subprocess.PIPE,+        stderr=subprocess.PIPE,+    )++    monitors = OrderedDict()+    for line in out.stdout.splitlines():+        mon = Monitor.parse_gohead(line)+        monitors[mon.nice_name] = mon+    return monitors+++def read_monitors_xrandr():+    '''+    Read monitor configuration directly from xrandr.++    This is useful for folks that don't have BurntSushi/gohead+    installed.+    '''+    out = subprocess.run(+        ['xrandr', '-q'],+        encoding='utf-8',+        stdout=subprocess.PIPE,+        stderr=subprocess.PIPE,+    )++    monitors = OrderedDict()+    index = 0+    for line in out.stdout.splitlines():+        # Make sure we only try to parse monitors that are not only+        # connected, but have an active resolution assigned to them.+        if not re.match(r'\S+\s+connected(\s+primary)?\s+[0-9]+', line):+            continue+        mon = Monitor.parse_xrandr(index, line)+        monitors[mon.nice_name] = mon+        index += 1+    return monitors+++def read_desktops():+    '''+    Read in all available desktops.++    The map returned is keyed by desktop index.+    '''+    out = subprocess.run(+        ['wmctrl', '-d'],+        encoding='utf-8',+        stdout=subprocess.PIPE,+        stderr=subprocess.PIPE,+    )++    desktops = OrderedDict()+    for line in out.stdout.splitlines():+        desk = Desktop.parse(line)+        desktops[desk.index] = desk+    return desktops+++def read_windows(mons, desks):+    '''+    Read in all visible windows.++    mons and desks must be the current set of monitors and+    desktops. Each window returned is attached to the+    corresponding monitor and desktop.+    '''+    out = subprocess.run(+        ['wmctrl', '-l', '-G'],+        encoding='utf-8',+        stdout=subprocess.PIPE,+        stderr=subprocess.PIPE,+    )++    unordered = {}+    for line in out.stdout.splitlines():+        win = Window.parse(line)+        unordered[win.id] = win++    windows = OrderedDict()+    for wid in read_stacking_window_ids():+        win = unordered[wid]+        for mon in mons.values():+            if mon.contains(win):+                win = win._replace(monitor=mon)+                break+        if not win.is_sticky():+            win = win._replace(desktop=desks[win.desktop_index])+            win.desktop.windows.append(win)+        windows[win.id] = win+    return windows+++def read_stacking_window_ids():+    '''+    Read a list of window IDs corresponding to the global stacking+    order of all open windows. The list returned is in bottom-to-top+    order.+    '''+    out = subprocess.run(+        ['xprop', '-root', '_NET_CLIENT_LIST_STACKING'],+        encoding='utf-8',+        stdout=subprocess.PIPE,+        stderr=subprocess.PIPE,+    )++    for m in re.finditer('0x[0-9a-fA-F]+', out.stdout):+        yield int(m.group(0), base=16)+++def current_desktop(desks):+    '''+    Find and return the currently active desktop in the given desktops.+    '''+    for d in desks.values():+        if d.current:+            return d+    return None+++def find_monitor(mons, name):+    '''+    Searches for a monitor by matching any one of its names, case+    insensitively, to the name given.+    '''+    for m in mons.values():+        if m.is_name_match(name):+            return m+    return None+++def cmd_focus_monitor(mons, desks, wins, monitor_name):+    '''+    Find the most recently used window on the monitor with the given+    name and focus it. Also, move the mouse to the top-left corner of+    the monitor.+    '''+    mon = find_monitor(mons, monitor_name)+    curdesk = current_desktop(desks)+    for w in reversed(curdesk.windows):+        if w.monitor is not None and w.monitor.index == mon.index:+            subprocess.run(['wmctrl', '-i', '-a', str(w.id)])+            break++    # And also move the mouse to the top-left corner of the+    # corresponding monitor.+    subprocess.run(['xdotool', 'mousemove', str(mon.x), str(mon.y)])+++if __name__ == '__main__':+    p = argparse.ArgumentParser(+        description='miscellaneous X11 commands for use in window managers '+                    'with shitty multi-monitor support'+    )+    subps = p.add_subparsers(dest='cmd')++    subp = subps.add_parser(+        'focus-monitor',+        description='Focus the most recently used window on the monitor '+                    'provided.',+    )+    subp.add_argument('monitor_name')++    args = p.parse_args()++    mons = read_monitors()+    desks = read_desktops()+    wins = read_windows(mons, desks)++    if args.cmd == 'focus-monitor':+        cmd_focus_monitor(mons, desks, wins, args.monitor_name)+    else:+        raise Exception(f'unrecognized command "{args.cmd}"')

bin/x11-go

new file mode 100644@@ -0,0 +1,3 @@+#!/bin/sh++. gopath "$HOME/.gox11"

bin/xauth-setup

new file mode 100755@@ -0,0 +1,8 @@+#!/bin/sh++if [ ! -f "$HOME/.Xauthority" ]; then+  touch $HOME/.Xauthority+  chmod 0600 $HOME/.Xauthority+fi++xauth add ":0.0" . $(mcookie)

bin/youtube-dl-mp3

new file mode 100755@@ -0,0 +1,3 @@+#!/bin/sh++exec youtube-dl --extract-audio --audio-format mp3 "$@"