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
@@ -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
@@ -0,0 +1,2 @@+/.git-old+/x86_64
@@ -0,0 +1,7 @@+#!/bin/sh++echo "nas+frink+leopard+jaguar+wiggum"
@@ -0,0 +1,8 @@+#!/bin/sh++exec runon-all \+ '(+ cd "$HOME"+ g pull --rebase+ pass git pull --rebase+ )'
@@ -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
@@ -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
@@ -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)))
@@ -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
@@ -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()
@@ -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
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++exec column -t -s' ' "$@"
@@ -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
@@ -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"
@@ -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
@@ -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
@@ -0,0 +1,6 @@+#!/bin/sh++set -e++/usr/bin/sync+sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
@@ -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
@@ -0,0 +1,6 @@+#!/bin/sh++if [ ! $# -eq 1 ]; then+ echo "usage: $(basename $0) command"+fi+command -v $1 > /dev/null 2>&1
@@ -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)
@@ -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
@@ -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'
@@ -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"
@@ -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"
@@ -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)
@@ -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)
@@ -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)
@@ -0,0 +1,4 @@+#!/bin/bash++find ./ -path './vendor' -prune -or -name '*.go' -print0 \+ | xargs -0 gotags > tags
@@ -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/");
@@ -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/
@@ -0,0 +1,5 @@+#!/usr/bin/env sh++find ./ -mindepth 1 -maxdepth 1 -type d |+ sed 's#^\./##g' |+ grep -v '^\.'
@@ -0,0 +1,3 @@+#!/bin/sh++chmod +x $*
@@ -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))
@@ -0,0 +1,5 @@+#!/bin/sh++no-blanking+xset +dpms+xset dpms 0 0 600
@@ -0,0 +1,3 @@+#!/bin/sh++/usr/bin/ogg123 /usr/share/sounds/KDE-Im-Nudge.ogg > /dev/null 2>&1
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++exec curl 'https://ipinfo.io/ip' 2> /dev/null
@@ -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
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++fc-list | sed 's,:.*,,' | sort -u
@@ -0,0 +1,3 @@+#!/bin/sh++sudo poweroff --force
@@ -0,0 +1,3 @@+#!/bin/sh++sudo reboot --force
@@ -0,0 +1,3 @@+#!/bin/sh++pwgen --capitalize --numerals --secure --ambiguous "$@" 20 1
@@ -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"
@@ -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"
@@ -0,0 +1,4 @@+#!/bin/sh++url="https://api.github.com/repos/BurntSushi/ripgrep/releases/latest"+curl -s "$url" | jq -r .tag_name
@@ -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 "$@"
@@ -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"
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++exec hyper-rg --engine hyperscan "$@"
@@ -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')
@@ -0,0 +1,3 @@+#!/bin/sh++exec darkhttpd /home/andrew/rust --port 8100 --default-mimetype image/svg+xml
@@ -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"
@@ -0,0 +1,5 @@+#!/bin/sh++url="$(grep "/$1 " | awk '{print $1}')"+echo "$url" | xclip -selection clipboard+echo "$url"
@@ -0,0 +1,3 @@+#!/bin/sh++exec hub pr list --sort-ascending -f '%pC%U - %<(15)%au - %t%n'
@@ -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
@@ -0,0 +1,4 @@+#!/bin/sh++clr=$(python3 -c "import random; print('%06x' % random.randint(0, 0xffffff))")+/usr/bin/i3lock --nofork --color "$clr" "$@"
@@ -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 "$@"
@@ -0,0 +1,6 @@+#!/bin/bash++if [[ "$(uname)" = *CYGWIN* ]]; then+ exit 0+fi+exit 1
@@ -0,0 +1,6 @@+#!/bin/sh++if [ -f "$HOME/.laptop" ]; then+ exit 0+fi+exit 1
@@ -0,0 +1,6 @@+#!/bin/sh++if [ "$(uname)" = "Darwin" ]; then+ exit 0+fi+exit 1
bin/is-remote-old
@@ -0,0 +1,7 @@+#!/bin/sh++if [ -f "$HOME/.iswork" ]; then+ exit 0+else+ exit 1+fi
@@ -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/
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++pandoc --from=markdown --to=rst "$@"
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -0,0 +1,3 @@+#!/bin/sh++watch -n 1 netstat -t -u
@@ -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]))
@@ -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
@@ -0,0 +1,6 @@+#!/bin/sh++while true; do+ no-blanking+ sleep 60+done
@@ -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
@@ -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)
@@ -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
@@ -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"
@@ -0,0 +1,6 @@+#!/bin/sh++rm -rf /etc/pacman.d/gnupg+pacman-key --init+pacman-key --populate archlinux+pacman-key --refresh-keys
@@ -0,0 +1,3 @@+#!/bin/sh++LC_ALL=en_US.UTF-8 exec /usr/bin/par "$@"
@@ -0,0 +1,3 @@+#!/bin/sh++exec nohup okular "$@" > /dev/null 2>&1 &
@@ -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" "$@")"
@@ -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"
@@ -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
@@ -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
@@ -0,0 +1,14 @@+#!/bin/bash++case "$1" in+*.pdf)+ if [ -s "$1" ]; then+ exec pdftotext "$1" -+ else+ exec cat+ fi+ ;;+*)+ exec cat+ ;;+esac
@@ -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();+'
@@ -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))
@@ -0,0 +1,3 @@+#!/bin/sh++RUST_BACKTRACE=1 "$@"
bin/remote-old-sync
bin/ren
@@ -0,0 +1,3 @@+#!/bin/sh++exec nvim -c Renamer
@@ -0,0 +1,3 @@+#!/bin/bash++echo -ne "\033[0m"
bin/restart-xcompmgr
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++exec nvim $(rg -l "$@")
@@ -0,0 +1,3 @@+#!/bin/sh++exec rg -p "$@" | less -RFX
@@ -0,0 +1,10 @@+#!/bin/sh++rsync \+ --archive \+ --recursive \+ --human-readable \+ --partial \+ --inplace \+ --progress \+ "$@"
@@ -0,0 +1,10 @@+#!/bin/sh++rsync \+ --recursive \+ --human-readable \+ --partial \+ --inplace \+ --progress \+ --size-only \+ "$@"
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@+#!/bin/sh++all-list | while read -r host; do+ echo "===== $host ====="+ runon "$host" "$@"+done
@@ -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
@@ -0,0 +1,8 @@+#!/bin/sh++kind=release+if [ -n "$1" ]; then+ kind="$1"+fi+PATH="$(pwd)/target/$kind:$PATH"+export PATH
@@ -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)\""
@@ -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"
@@ -0,0 +1,3 @@+#!/bin/sh++exec msmtp "$@"
@@ -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
@@ -0,0 +1,4 @@+#!/bin/bash++ntpdate pool.ntp.org+hwclock --systohc
@@ -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
@@ -0,0 +1,8 @@+#!/bin/bash++if [ $# = 0 ]; then+ dir=.+else+ dir=("$@")+fi+du -achx -d 1 "${dir[@]}" | sort -h
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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
@@ -0,0 +1,4 @@+#!/bin/sh++sleep 1+xset dpms force standby
@@ -0,0 +1,3 @@+#!/bin/sh++rscp "$@" bs:~/www/burntsushi.net/stuff/
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -0,0 +1,8 @@+#!/bin/sh++d="$1"+shift++mkdir "$d"+cp -a "$@" "$d"/+tar zcf "$d.tar.gz" "$d"
@@ -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
@@ -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+
@@ -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
@@ -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 - -+
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++"$HOME/bin/terminal" -e tmux -u new zsh
@@ -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"
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@+#!/bin/sh++path="$1"+tmppath=$(mktemp)+cp "$path" "$tmppath"+iconv -f UTF-8 -t UTF-16 "$tmppath" > "$path"+rm "$tmppath"
@@ -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
@@ -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+
@@ -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"
@@ -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
@@ -0,0 +1,3 @@+#!/bin/sh++video-scale 720 "$@"
@@ -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"
@@ -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
@@ -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
@@ -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}"')
@@ -0,0 +1,3 @@+#!/bin/sh++. gopath "$HOME/.gox11"
@@ -0,0 +1,8 @@+#!/bin/sh++if [ ! -f "$HOME/.Xauthority" ]; then+ touch $HOME/.Xauthority+ chmod 0600 $HOME/.Xauthority+fi++xauth add ":0.0" . $(mcookie)
@@ -0,0 +1,3 @@+#!/bin/sh++exec youtube-dl --extract-audio --audio-format mp3 "$@"