aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--bin/executable_Internet.m3u.sh40
-rwxr-xr-xbin/executable_copyright.awk122
-rwxr-xr-xbin/executable_do_blue.sh4
-rwxr-xr-xbin/executable_do_dac.sh19
-rwxr-xr-xbin/executable_do_speakers.sh13
-rwxr-xr-xbin/executable_icd112
-rwxr-xr-xbin/executable_lbdb-fetchaddr-wrapper7
-rwxr-xr-xbin/executable_mailx-alias13
-rwxr-xr-xbin/executable_mice.sh25
-rwxr-xr-xbin/executable_mpd_only.sh7
-rwxr-xr-xbin/executable_mpd_others.sh10
-rwxr-xr-xbin/executable_mutt-fetchbug161
-rwxr-xr-xbin/executable_mutt_bgrun115
-rwxr-xr-xbin/executable_mutt_oauth2.py419
-rwxr-xr-xbin/executable_ptmp50
-rwxr-xr-xbin/executable_startaudio3
-rwxr-xr-xbin/executable_tmcg29
-rwxr-xr-xbin/executable_wd-bak14
-rwxr-xr-xbin/executable_wd-mount18
-rw-r--r--bin/executable_wd-umount21
20 files changed, 1202 insertions, 0 deletions
diff --git a/bin/executable_Internet.m3u.sh b/bin/executable_Internet.m3u.sh
new file mode 100644
index 0000000..b417f07
--- /dev/null
+++ b/bin/executable_Internet.m3u.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Updates the internet radio playlists for MPD
+# Best run from a cronjob
+
+BASE=/var/lib/mpd/playlists/
+
+playlist() {
+ cat<<EOF > "${BASE}/radio-$1.m3u"
+#EXTM3U
+#EXTINF:-1,$2
+$3
+EOF
+}
+
+getstream() {
+ curl -s "$1" | grep '^File1=' | sed -e 's/File1=//g'
+}
+
+playlist "1920s" "1920s Radio Network" "$(getstream 'http://kara.fast-serv.com:8398/listen.pls')"
+# playlist "CFSF-FM" "CFSF-FM 99.3 (Sturgeon Falls)" 'http://listenlive.vistaradio.ca/CFSF'
+# playlist "CHYK-FM" "CHYK-FM 104.1 (Timmins)" 'http://rubix.wavestreamer.com:8015/stream/1/'
+playlist "CINN-FM" "CINN-FM 99.1 (Hearst)" 'http://stream2.statsradio.com:8050/stream'
+# playlist "CJFO-FM" "CJFO-FM 94.5 (Ottawa)" 'http://stream03.ustream.ca:8000/cjfofm128.mp3'
+playlist "CKGN-FM" "CKGN-FM 89.7 (Kapuskasing)" 'http://stream03.ustream.ca:80/ckgn128.mp3'
+playlist "WYEP" "WYEP 91.3 (Pittsburgh)" 'https://ais-sa3.cdnstream1.com/2557_128.mp3'
+playlist "WZUM" "WZUM 88.1 (Pittsburgh)" 'http://pubmusic.streamguys1.com/wzum-aac'
+playlist "Dismuke" "Radio Dismuke" "$(getstream 'https://early1900s.org/radiodismuke/radiodismuke.pls')"
+playlist "russhit" "99.6 Радио Русский Хит" "$(curl -s http://ruhit.fm/player.htm | grep ruhit_64 | sed -e 's/.*="//g;s/".*//g')"
+
+# http://colombiacrossover.com/
+playlist "salsa.dura" "Colombia Salsa Dura" "$(getstream 'http://64.37.50.226:8054/listen.pls?sid=1')"
+
+# http://www.tenientiko.com/
+playlist "salsa.catedral" "La Catedral de la Salsa" "$(getstream 'http://176.31.120.166:4450/listen.pls?sid=1')"
+
+# http://www.rockolapegassera.com
+playlist "cumbia.rockola" "Rockola Pegassera 107.9" "$(getstream 'http://54.39.19.215:8004/listen.pls?sid=1')"
+
+playlist "salsa.metro" "El Metro Salsero" 'http://s5.voscast.com:7516/stream'
diff --git a/bin/executable_copyright.awk b/bin/executable_copyright.awk
new file mode 100755
index 0000000..371a9c2
--- /dev/null
+++ b/bin/executable_copyright.awk
@@ -0,0 +1,122 @@
+#!/usr/bin/gawk -f
+# Copyright (C) 2013 Ryan Kavanagh <rak@debian.org>
+# Given a series of lines in the format
+# Copyright (c) NNNN, MMMM-MMMM, ..., NNNN John Smith <jsmith@example.org>
+# group years and emails by person.
+
+{
+ match($0, /.*Copyright.*[0-9][,]? +/);
+ DATE_LENGTH = RLENGTH;
+ match($0, /<.*>/);
+ EMAIL_START = RSTART;
+ if (RLENGTH != -1) {
+ NAME = substr($0, DATE_LENGTH + 1, EMAIL_START - DATE_LENGTH - 2);
+ EMAIL = substr($0, EMAIL_START);
+ } else {
+ # No email on this line
+ NAME = substr($0, DATE_LENGTH + 1);
+ }
+ match($0, /.*Copyright +\([cC]\) +/);
+ DATE_START = RLENGTH + 1;
+ YEARS = substr($0, DATE_START, DATE_LENGTH - DATE_START);
+ gsub(/, +/, " ", YEARS);
+ gsub(/,/, " ", YEARS);
+ people_years[NAME] = people_years[NAME] " " YEARS;
+ if (EMAIL_LENGTH != -1) {
+ email_pattern = "/.*" EMAIL ".*/";
+ if (!(NAME in people_emails)) {
+ people_emails[NAME] = EMAIL;
+ } else if (!match(people_emails[NAME], EMAIL)) {
+ people_emails[NAME] = people_emails[NAME] "," EMAIL;
+ }
+ }
+} END {
+ for (person in people_years) {
+ delete years_array;
+ split(people_years[person], years_array);
+ # Split any hyphenated years;
+ for (year in years_array) {
+ if (years_array[year] ~ /[0-9]+-[0-9]+/) {
+ delete split_year;
+ split(years_array[year], split_year, /-/);
+ years_array[year] = split_year[1];
+ if (split_year[1] != split_year[2]) {
+ # Make sure it isn't some crappy input like 2012-2012
+ for (j = 1; j <= split_year[2] - split_year[1]; j++) {
+ years_array[length(years_array) + 1] = \
+ years_array[year] + j;
+ }
+ }
+ }
+ }
+ # Sort the years
+ asort(years_array);
+ # Delete any duplicates:
+ for (i = 1; i <= length(years_array); i++) {
+ if (i > 1 && years_array[i-1] == years_array[i]) {
+ # Delete years_array[i-1] instead of years_array[i] so that we
+ # can still check the next year with ease
+ delete years_array[i-1];
+ }
+ }
+ # Final sort
+ asort(years_array);
+ # Remove duplicates and generate year string
+ year_string = "";
+ # Force AWK to access the years in order
+ added_hyphen = 0;
+ for (i = 1; i <= length(years_array); i++) {
+ if (i > 1) {
+ if (years_array[i - 1] != years_array[i]) {
+ # added_hyphen tracks if the last character in the string is
+ # a hyphen
+ if ((!added_hyphen) && (years_array[i - 1] == years_array[i] - 1)) {
+ # year_string isn't terminated by a hyphen, and the year
+ # at i-1 is one less than the current one
+ year_string = year_string "-";
+ added_hyphen = 1;
+ } else if (added_hyphen && (years_array[i - 1] != years_array[i] - 1)) {
+ # The string is terminated by a hyphen, but the current
+ # year does not immediately follow the preceeding
+ # one
+ year_string = year_string years_array[i-1] ", " years_array[i];
+ added_hyphen = 0;
+ } else if (!added_hyphen) {
+ year_string = year_string ", " years_array[i];
+ }
+ }
+ } else {
+ year_string = years_array[i];
+ }
+ }
+ # We've added a hyphen, but run out of years to check, terminate it
+ if (added_hyphen) {
+ year_string = year_string years_array[length(years_array)];
+ }
+ final_line[years_array[length(years_array)]][length(years_array)][person] = \
+ "Copyright (C) " year_string "\t" person " " people_emails[person];
+ }
+ # We can't sort the years indices with asorti because we want a numerical,
+ # not lexicographic sort of the indices.
+ j = 0;
+ delete years_sorted;
+ for (i in final_line) years_sorted[j++] = i+0;
+ n_years_entries = asort(years_sorted);
+ # And output the lines with the most recent contributor first
+ for (y = n_years_entries; y >= 1; y--) {
+ # Sort the contributors with most recent contribution in year
+ # by_year[y] by number of years contributed:
+ j = 0;
+ delete contributions_sorted;
+ for (i in final_line[years_sorted[y]]) contributions_sorted[j++] = i+0;
+ n_contrib_entries = asort(contributions_sorted);
+ for (c = n_contrib_entries; c >= 1; c--) {
+ # Finally, sort by contributor name
+ asorti(final_line[years_sorted[y]][contributions_sorted[c]], by_person);
+ # And output the lines in alphabetical order by person name
+ for (n = 1; n <= length(by_person); n++) {
+ print final_line[years_sorted[y]][contributions_sorted[c]][by_person[n]];
+ }
+ }
+ }
+}
diff --git a/bin/executable_do_blue.sh b/bin/executable_do_blue.sh
new file mode 100755
index 0000000..b908d14
--- /dev/null
+++ b/bin/executable_do_blue.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+pacmd list-modules module-bluez5-discover | grep -q module-bluez5-discover || \
+ pacmd load-module module-bluez5-discover
+echo "connect CD:0D:69:69:9A:1B" | bluetoothctl
diff --git a/bin/executable_do_dac.sh b/bin/executable_do_dac.sh
new file mode 100755
index 0000000..7ae913e
--- /dev/null
+++ b/bin/executable_do_dac.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+USB_CARD="alsa_card.usb-FiiO_DigiHug_USB_Audio-01"
+USB_SINK="alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo"
+SPEAKERS="alsa_output.pci-0000_00_1b.0.analog-stereo"
+
+pacmd set-sink-mute "${SPEAKERS}" 1
+pacmd set-card-profile "${USB_CARD}" output:iec958-stereo
+pacmd set-sink-mute "${USB_SINK}" 0
+
+if pacmd list-modules | grep module-ladspa-sink; then
+ pacmd unload-module module-ladspa-sink
+fi
+pacmd load-module module-ladspa-sink sink_name=binaural sink_master="${USB_SINK}" plugin=bs2b label=bs2b control=700,4.5
+
+for s in $(pacmd list-sink-inputs | awk '$1 == "index:" {print $2}')
+do
+ pacmd move-sink-input $s "${USB_SINK}" >/dev/null 2>&1
+done
diff --git a/bin/executable_do_speakers.sh b/bin/executable_do_speakers.sh
new file mode 100755
index 0000000..84b82e1
--- /dev/null
+++ b/bin/executable_do_speakers.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+USB_CARD="alsa_card.usb-FiiO_DigiHug_USB_Audio-01"
+USB_SINK="alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo"
+SPEAKERS="alsa_output.pci-0000_00_1b.0.analog-stereo"
+
+pacmd set-sink-mute "${USB_SINK}" 1
+pacmd set-sink-mute "${SPEAKERS}" 0
+
+for s in $(pacmd list-sink-inputs | awk '$1 == "index:" {print $2}')
+do
+ pacmd move-sink-input $s "${SPEAKERS}" >/dev/null 2>&1
+done
diff --git a/bin/executable_icd b/bin/executable_icd
new file mode 100755
index 0000000..367b87f
--- /dev/null
+++ b/bin/executable_icd
@@ -0,0 +1,112 @@
+#!/bin/sh
+
+XKB_DIR=${HOME}/.xkb
+[ -d ${XKB_DIR}/keymap ] || mkdir -p ${XKB_DIR}/keymap
+
+GLOBAL_OPTIONS="\
+ -I${XKB_DIR} \
+ -layout icd,ru \
+ -variant icd, \
+ -option terminate:ctrl_alt_bksp \
+ -option nbsp:level3n \
+ -option lalt_meta:lalt_meta \
+ -option grp:shifts_toggle"
+
+LAPTOP_OPTIONS="\
+ ${GLOBAL_OPTIONS} \
+ -option lv3:ralt_switch_multikey"
+
+KIN_OPTIONS="\
+ ${GLOBAL_OPTIONS} \
+ -option lv3:switch \
+ -option caps:swapescape"
+
+SUN_OPTIONS="\
+ ${GLOBAL_OPTIONS} \
+ -geometry sun(type6unix) \
+ -option caps:escape \
+ -option lv3:menu_switch \
+ -option myswap:switch_lalt_lsuper"
+
+ERG_OPTIONS="\
+ ${GLOBAL_OPTIONS} \
+ -option lv3:switch \
+ -option caps:escape"
+
+
+case `uname` in
+ OpenBSD)
+ LAPTOP_ID=$(xinput | grep "/dev/wskbd" | sed -e 's/.*id=\([0-9]\+\).*/\1/g')
+ ;;
+ Linux)
+ LAPTOP_ID=$(xinput | grep "AT Translated Set 2 keyboard" | sed -e 's/.*id=\([0-9]\+\).*/\1/g')
+ KIN_USB_ID=$(lsusb | grep -i "Kinesis Advantage Pro" | awk '{ print $6 }')
+ ERG_USB_ID=$(lsusb | grep -i "feed:1307" | awk '{ print $6 }')
+ SUN_USB_ID=$(lsusb | grep -i "Sun Microsystems, Inc. Type 6 Keyboard" | awk '{ print $6 }')
+ ;;
+ *)
+ ;;
+esac
+
+echo "Setting up laptop"
+setxkbmap ${LAPTOP_OPTIONS} -device ${LAPTOP_ID} -print > ${XKB_DIR}/keymap/icd.laptop
+# xkbcomp -I${HOME}/.xkb -i ${LAPTOP_ID} -synch \
+xkbcomp -I${HOME}/.xkb -synch \
+ ${HOME}/.xkb/keymap/icd.laptop $DISPLAY # 2> /dev/null
+
+if [ "x${KIN_USB_ID}" != "x" ]; then
+ echo "Setting up Kinesis"
+ KIN_XINPUT_ID=$(xinput | grep ${KIN_USB_ID} | sed -e 's/.*id=\([0-9]\+\).*/\1/g')
+ for XID in $KIN_XINPUT_ID; do
+ echo $XID
+ setxkbmap \
+ -I${XKB_DIR} \
+ -device ${XID} \
+ ${KIN_OPTIONS} \
+ -print | sed -e 's@+ctrl(nocaps)@@g;s@bksp)@bksp)+lalt_meta(lalt_meta)@g' > ${HOME}/.xkb/keymap/icd.kin
+ xkbcomp -I${HOME}/.xkb -i ${XID} -synch \
+ ${HOME}/.xkb/keymap/icd.kin ${DISPLAY} # 2> /dev/null
+ done
+ xkbcomp -I${HOME}/.xkb -synch \
+ ${HOME}/.xkb/keymap/icd.kin ${DISPLAY} # 2> /dev/null
+ xmodmap -e "remove mod1 = Alt_R"
+ xmodmap -e "add mod4 = Alt_R"
+fi
+
+if [ "x${SUN_USB_ID}" != "x" ]; then
+ echo "Setting up Sun Type 6"
+ SUN_XINPUT_ID=$(xinput | grep ${SUN_USB_ID} | sed -e 's/.*id=\([0-9]\+\).*/\1/g')
+ echo "ID: ${SUN_XINPUT_ID}"
+ for XID in $SUN_XINPUT_ID; do
+ echo $XID
+ setxkbmap \
+ -I${XKB_DIR} \
+ -device ${XID} \
+ ${SUN_OPTIONS} \
+ -print | sed -e '/xkb_keycodes/s/"[[:space:]]/+sunt6fix&/' > ${HOME}/.xkb/keymap/icd.sun
+ xkbcomp -I${HOME}/.xkb -i ${XID} -synch \
+ ${HOME}/.xkb/keymap/icd.sun ${DISPLAY} # 2> /dev/null
+ done
+fi
+
+echo ${ERG_USB_ID}
+if [ "x${ERG_USB_ID}" != "x" ]; then
+ echo "Setting up ergodox"
+ ERG_XINPUT_ID=$(xinput | grep "ErgoDox EZ" | grep keyboard | sed -e 's/.*id=\([0-9]\+\).*/\1/g')
+ echo "ID: ${ERG_XINPUT_ID}"
+ for XID in $ERG_XINPUT_ID; do
+ echo $XID
+ setxkbmap \
+ -I${XKB_DIR} \
+ -device ${XID} \
+ ${ERG_OPTIONS} \
+ -print > ${HOME}/.xkb/keymap/icd.erg
+ # -print | sed -e 's@+group(shifts_toggle)@+ctrl(nocaps)&@g' > ${HOME}/.xkb/keymap/icd.erg
+ # xkbcomp -I${HOME}/.xkb -i ${XID} -synch \
+ xkbcomp -I${HOME}/.xkb -synch \
+ ${HOME}/.xkb/keymap/icd.erg ${DISPLAY} # 2> /dev/null
+ done
+fi
+
+
+echo icd > ${HOME}/.xmonad/layout
diff --git a/bin/executable_lbdb-fetchaddr-wrapper b/bin/executable_lbdb-fetchaddr-wrapper
new file mode 100755
index 0000000..bb1270a
--- /dev/null
+++ b/bin/executable_lbdb-fetchaddr-wrapper
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if command -v lbdb-fetchaddr > /dev/null; then
+ { tee /dev/fd/3 | lbdb-fetchaddr >&2; } 3>&1
+else
+ cat
+fi
diff --git a/bin/executable_mailx-alias b/bin/executable_mailx-alias
new file mode 100755
index 0000000..c1d961a
--- /dev/null
+++ b/bin/executable_mailx-alias
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+MUTT_ALIAS=${HOME}/.mutt/alias.rc
+MAILX_ALIAS=${HOME}/.mailx-aliases.rc
+
+awk '{
+ NAME="";
+ for (i = 3; i < NF; i++) {
+ NAME=NAME " " $i;
+ };
+ NAME = substr(NAME, 2);
+ print "alias", $2, "\"" $NF, "(" NAME ")\"";
+}' $MUTT_ALIAS > $MAILX_ALIAS
diff --git a/bin/executable_mice.sh b/bin/executable_mice.sh
new file mode 100755
index 0000000..794f86f
--- /dev/null
+++ b/bin/executable_mice.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+# Synaptics:
+synclient HorizTwoFingerScroll=0 || true
+synclient HorizEdgeScroll=1 || true
+# Enable circular scrolling with top edge activating
+synclient CircularScrolling=1 || true
+synclient CircScrollTrigger=1 || true
+# One finger is left click
+synclient TapButton1=1 || true
+# Two is right click
+synclient TapButton2=3 || true
+# Three is middle click
+synclient TapButton3=2 || true
+# Enable coasting
+synclient CoastingSpeed=5
+synclient CoastingFriction=30
+
+trackball=$(xinput | grep "Kensington Expert Wireless TB" | grep pointer | sed -e 's/.*id=//g;s/\s\+.*//g')
+if [ "x${trackball}" != "x" ]; then
+ xinput set-button-map "${trackball}" 1 2 8 4 5 6 7 3 9 10 11 12 13 14 15 16
+ xinput set-prop "${trackball}" "libinput Accel Speed" 0.25
+fi
+
+
diff --git a/bin/executable_mpd_only.sh b/bin/executable_mpd_only.sh
new file mode 100755
index 0000000..468c6a5
--- /dev/null
+++ b/bin/executable_mpd_only.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -e
+
+systemctl --user stop pulseaudio.socket
+mpc enable "DigiHug USB Audio"
+mpc disable "My Pulse Output"
diff --git a/bin/executable_mpd_others.sh b/bin/executable_mpd_others.sh
new file mode 100755
index 0000000..b27e11e
--- /dev/null
+++ b/bin/executable_mpd_others.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -e
+
+systemctl --user start pulseaudio.socket
+systemctl --user start pulseaudio.service
+mpc disable "DigiHug USB Audio"
+mpc enable "My Pulse Output"
+pacmd load-module module-udev-detect
+${HOME}/bin/do_dac.sh
diff --git a/bin/executable_mutt-fetchbug b/bin/executable_mutt-fetchbug
new file mode 100755
index 0000000..93ffc58
--- /dev/null
+++ b/bin/executable_mutt-fetchbug
@@ -0,0 +1,161 @@
+#!/usr/bin/perl -w
+#
+# mutt-fetchbug, extensively based off of
+# mutt-notmuch - notmuch (of a) helper for Mutt
+#
+# Copyright: © 2011 Stefano Zacchiroli <zack@upsilon.cc>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# Differences between mutt-notmuch and mutt-fetchbug are
+# Copyright: © 2012 Ryan Kavanagh <rak@debian.org>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# See the bottom of this file for more documentation.
+# A manpage can be obtained by running "pod2man mutt-fetchbug > mutt-fetchbug.1"
+
+use strict;
+use warnings;
+
+use File::Path;
+use Getopt::Long;
+use Pod::Usage;
+
+# search($btsmbox, $query)
+# Fetch bugs matching $query with bts; store results in $btsmbox
+sub search($$) {
+ my ($btsmbox, $query) = @_;
+
+ system("bts --cache-mode=mbox cache $query"
+ . " && ln -fs ~/.cache/devscripts/bts/$query.mbox $btsmbox");
+}
+
+sub search_action($$@) {
+ my ($interactive, $btsmbox, @params) = @_;
+
+ if (! $interactive) {
+ fetch($btsmbox, join(' ', @params));
+ } else {
+ my $query = "";
+ my $done = 0;
+ while (! $done) {
+ print "bug number ('?' for man): ";
+ chomp($query = <STDIN>);
+ if ($query eq "?") {
+ system("man bts");
+ } elsif ($query eq "") {
+ $done = 1; # quit doing nothing
+ } else {
+ search($btsmbox, $query);
+ $done = 1;
+ }
+ }
+ }
+}
+
+sub die_usage() {
+ my %podflags = ( "verbose" => 1,
+ "exitval" => 2 );
+ pod2usage(%podflags);
+}
+
+sub main() {
+ my $btsmbox = "$ENV{HOME}/.cache/mutt_btsresults";
+ my $interactive = 0;
+ my $help_needed = 0;
+
+ my $getopt = GetOptions(
+ "h|help" => \$help_needed,
+ "o|output-mbox=s" => \$btsmbox,
+ "p|prompt" => \$interactive);
+ if (! $getopt || $#ARGV < 0) { die_usage() };
+ my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
+
+ if ($help_needed) {
+ die_usage();
+ } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
+ print STDERR "Error: no search term provided\n\n";
+ die_usage();
+ } elsif ($action eq "search") {
+ search_action($interactive, $btsmbox, @params);
+ } else {
+ die_usage();
+ }
+}
+
+main();
+
+__END__
+
+=head1 NAME
+
+mutt-fetchbug - 'bts show' frontend for Mutt
+
+=head1 SYNOPSIS
+
+=over
+
+=item B<mutt-fetchbug> [I<OPTION>]... search [I<SEARCH-TERM>]...
+
+=back
+
+=head1 DESCRIPTION
+
+mutt-fetchbug is a frontend to the 'bts show' command (Debian package:
+devscripts) designed to fetch bugs and place them in a predefined mbox. The
+search term should typically be a bug number.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -o DIR
+
+=item --output-mbox DIR
+
+Store search results as (symlink) mbox MBOX. Beware: MBOX will be overwritten.
+(Default: F<~/.cache/mutt_btsresults/>)
+
+=item -p
+
+=item --prompt
+
+Instead of using command line search terms, prompt the user for them (only for
+"search").
+
+=item -h
+
+=item --help
+
+Show usage information and exit.
+
+=back
+
+=head1 INTEGRATION WITH MUTT
+
+mutt-fetchbug can be used to integrate 'bts show' with the Mutt mail user agent
+(unsurprisingly, given the name). To that end, you should define the following
+macros in your F<~/.muttrc> (replacing F<~/bin/mutt-fetchbug> for the actual
+location of mutt-fetchbug on your system):
+
+ macro index <F7> \
+ "<enter-command>unset wait_key<enter><shell-escape>~/bin/mutt-fetchbug --prompt search<enter><change-folder-readonly>~/.cache/mutt_btsresults<enter><enter-command>set wait_key<enter>" \
+ "fetch bug(s) (using bts show)"
+
+The macro (activated by <F7>) will prompt the user for a bug number and then
+jump to a temporary mbox showing the fetched bug.
+
+=head1 SEE ALSO
+
+mutt(1), bts(1)
+
+=head1 AUTHOR
+
+mutt-fetchbug is extensively based off of 'mutt-notmuch', which is
+Copyright: (C) 2011 Stefano Zacchiroli <zack@upsilon.cc>.
+
+All differences between mutt-fetchbug and mutt-notmuch are
+Copyright (C) 2012 Ryan Kavanagh <rak@debian.org>
+
+License: GNU General Public License (GPL), version 3 or higher
+
+=cut
diff --git a/bin/executable_mutt_bgrun b/bin/executable_mutt_bgrun
new file mode 100755
index 0000000..f833bab
--- /dev/null
+++ b/bin/executable_mutt_bgrun
@@ -0,0 +1,115 @@
+#!/bin/sh
+# @(#) mutt_bgrun $Revision: 1.4 $
+
+# mutt_bgrun - run an attachment viewer from mutt in the background
+# Copyright (C) 1999-2002 Gary A. Johnson
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# SYNOPSIS
+# mutt_bgrun viewer [viewer options] file
+#
+# DESCRIPTION
+# Mutt invokes external attachment viewers by writing the
+# attachment to a temporary file, executing the pipeline specified
+# for that attachment type in the mailcap file, waiting for the
+# pipeline to terminate, writing nulls over the temporary file,
+# then deleting it. This causes problems when using graphical
+# viewers such as qvpview and acroread to view attachments.
+#
+# If qvpview, for example, is executed in the foreground, the mutt
+# user interface is hung until qvpview exits, so the user can't do
+# anything else with mutt until he or she finishes reading the
+# attachment and exits qvpview. This is especially annoying when
+# a message contains several MS Office attachments--one would like
+# to have them all open at once.
+#
+# If qvpview is executed in the background, it must be given
+# enough time to completely read the file before returning control
+# to mutt, since mutt will then obliterate the file. Qvpview is
+# so slow that this time can exceed 20 seconds, and the bound is
+# unknown. So this is again annoying.
+#
+# The solution provided here is to invoke the specified viewer
+# from this script after first copying mutt's temporary file to
+# another temporary file. This script can then quickly return
+# control to mutt while the viewer can take as much time as it
+# needs to read and render the attachment.
+#
+# EXAMPLE
+# To use qvpview to view MS Office attachments from mutt, add the
+# following lines to mutt's mailcap file.
+#
+# application/msword; mutt_bgrun qvpview %s
+# application/vnd.ms-excel; mutt_bgrun qvpview %s
+# application/vnd.ms-powerpoint; mutt_bgrun qvpview %s
+#
+# AUTHOR
+# Gary A. Johnson
+# <garyjohn@spk.agilent.com>
+#
+# ACKNOWLEDGEMENTS
+# My thanks to the people who have commented on this script and
+# offered solutions to shortcomings and bugs, especially Edmund
+# GRIMLEY EVANS <edmundo@rano.org> and Andreas Somogyi
+# <aso@somogyi.nu>.
+
+prog=${0##*/}
+
+# Check the arguments first.
+
+if [ "$#" -lt "2" ]
+then
+ echo "usage: $prog viewer [viewer options] file" >&2
+ exit 1
+fi
+
+# Separate the arguments. Assume the first is the viewer, the last is
+# the file, and all in between are options to the viewer.
+
+viewer="$1"
+shift
+
+while [ "$#" -gt "1" ]
+do
+ options="$options $1"
+ shift
+done
+
+file=$1
+
+# Create a temporary directory for our copy of the temporary file.
+#
+# This is more secure than creating a temporary file in an existing
+# directory.
+
+tmpdir=/tmp/$LOGNAME$$
+umask 077
+mkdir "$tmpdir" || exit 1
+tmpfile="$tmpdir/${file##*/}"
+
+# Copy mutt's temporary file to our temporary directory so that we can
+# let mutt overwrite and delete it when we exit.
+
+cp "$file" "$tmpfile"
+
+# Run the viewer in the background and delete the temporary files when done.
+
+(
+ "$viewer" $options "$tmpfile"
+ sleep 2
+ rm -f "$tmpfile"
+ rmdir "$tmpdir"
+) &
diff --git a/bin/executable_mutt_oauth2.py b/bin/executable_mutt_oauth2.py
new file mode 100755
index 0000000..f67364e
--- /dev/null
+++ b/bin/executable_mutt_oauth2.py
@@ -0,0 +1,419 @@
+#!/usr/bin/env python3
+#
+# Mutt OAuth2 token management script, version 2020-08-07
+# Written against python 3.7.3, not tried with earlier python versions.
+#
+# Copyright (C) 2020 Alexander Perlis
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+'''Mutt OAuth2 token management'''
+
+import sys
+import json
+import argparse
+import urllib.parse
+import urllib.request
+import imaplib
+import poplib
+import smtplib
+import base64
+import secrets
+import hashlib
+import time
+from datetime import timedelta, datetime
+from pathlib import Path
+import socket
+import http.server
+import subprocess
+
+# The token file must be encrypted because it contains multi-use bearer tokens
+# whose usage does not require additional verification. Specify whichever
+# encryption and decryption pipes you prefer. They should read from standard
+# input and write to standard output. The example values here invoke GPG,
+# although won't work until an appropriate identity appears in the first line.
+ENCRYPTION_PIPE = ['cat']
+DECRYPTION_PIPE = ['cat']
+
+registrations = {
+ 'google': {
+ 'authorize_endpoint': 'https://accounts.google.com/o/oauth2/auth',
+ 'devicecode_endpoint': 'https://oauth2.googleapis.com/device/code',
+ 'token_endpoint': 'https://accounts.google.com/o/oauth2/token',
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
+ 'imap_endpoint': 'imap.gmail.com',
+ 'pop_endpoint': 'pop.gmail.com',
+ 'smtp_endpoint': 'smtp.gmail.com',
+ 'sasl_method': 'OAUTHBEARER',
+ 'scope': 'https://mail.google.com/',
+ 'client_id': '',
+ 'client_secret': '',
+ },
+ 'microsoft': {
+ 'authorize_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
+ 'devicecode_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
+ 'token_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
+ 'redirect_uri': 'https://login.microsoftonline.com/common/oauth2/nativeclient',
+ 'tenant': 'common',
+ 'imap_endpoint': 'outlook.office365.com',
+ 'pop_endpoint': 'outlook.office365.com',
+ 'smtp_endpoint': 'smtp.office365.com',
+ 'sasl_method': 'XOAUTH2',
+ 'scope': ('offline_access https://outlook.office.com/IMAP.AccessAsUser.All '
+ 'https://outlook.office.com/POP.AccessAsUser.All '
+ 'https://outlook.office.com/SMTP.Send'),
+ 'client_id': '08162f7c-0fd2-4200-a84a-f25a4db0b584',
+ 'client_secret': 'TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82',
+ },
+}
+
+ap = argparse.ArgumentParser(epilog='''
+This script obtains and prints a valid OAuth2 access token. State is maintained in an
+encrypted TOKENFILE. Run with "--verbose --authorize" to get started or whenever all
+tokens have expired, optionally with "--authflow" to override the default authorization
+flow. To truly start over from scratch, first delete TOKENFILE. Use "--verbose --test"
+to test the IMAP/POP/SMTP endpoints.
+''')
+ap.add_argument('-v', '--verbose', action='store_true', help='increase verbosity')
+ap.add_argument('-d', '--debug', action='store_true', help='enable debug output')
+ap.add_argument('tokenfile', help='persistent token storage')
+ap.add_argument('-a', '--authorize', action='store_true', help='manually authorize new tokens')
+ap.add_argument('--authflow', help='authcode | localhostauthcode | devicecode')
+ap.add_argument('-t', '--test', action='store_true', help='test IMAP/POP/SMTP endpoints')
+args = ap.parse_args()
+
+token = {}
+path = Path(args.tokenfile)
+if path.exists():
+ if 0o777 & path.stat().st_mode != 0o600:
+ sys.exit('Token file has unsafe mode. Suggest deleting and starting over.')
+ try:
+ sub = subprocess.run(DECRYPTION_PIPE, check=True, input=path.read_bytes(),
+ capture_output=True)
+ token = json.loads(sub.stdout)
+ except subprocess.CalledProcessError:
+ sys.exit('Difficulty decrypting token file. Is your decryption agent primed for '
+ 'non-interactive usage, or an appropriate environment variable such as '
+ 'GPG_TTY set to allow interactive agent usage from inside a pipe?')
+
+
+def writetokenfile():
+ '''Writes global token dictionary into token file.'''
+ if not path.exists():
+ path.touch(mode=0o600)
+ if 0o777 & path.stat().st_mode != 0o600:
+ sys.exit('Token file has unsafe mode. Suggest deleting and starting over.')
+ sub2 = subprocess.run(ENCRYPTION_PIPE, check=True, input=json.dumps(token).encode(),
+ capture_output=True)
+ path.write_bytes(sub2.stdout)
+
+
+if args.debug:
+ print('Obtained from token file:', json.dumps(token))
+if not token:
+ if not args.authorize:
+ sys.exit('You must run script with "--authorize" at least once.')
+ print('Available app and endpoint registrations:', *registrations)
+ token['registration'] = input('OAuth2 registration: ')
+ token['authflow'] = input('Preferred OAuth2 flow ("authcode" or "localhostauthcode" '
+ 'or "devicecode"): ')
+ token['email'] = input('Account e-mail address: ')
+ token['access_token'] = ''
+ token['access_token_expiration'] = ''
+ token['refresh_token'] = ''
+ writetokenfile()
+
+if token['registration'] not in registrations:
+ sys.exit(f'ERROR: Unknown registration "{token["registration"]}". Delete token file '
+ f'and start over.')
+registration = registrations[token['registration']]
+
+authflow = token['authflow']
+if args.authflow:
+ authflow = args.authflow
+
+baseparams = {'client_id': registration['client_id']}
+# Microsoft uses 'tenant' but Google does not
+if 'tenant' in registration:
+ baseparams['tenant'] = registration['tenant']
+
+
+def access_token_valid():
+ '''Returns True when stored access token exists and is still valid at this time.'''
+ token_exp = token['access_token_expiration']
+ return token_exp and datetime.now() < datetime.fromisoformat(token_exp)
+
+
+def update_tokens(r):
+ '''Takes a response dictionary, extracts tokens out of it, and updates token file.'''
+ token['access_token'] = r['access_token']
+ token['access_token_expiration'] = (datetime.now() +
+ timedelta(seconds=int(r['expires_in']))).isoformat()
+ if 'refresh_token' in r:
+ token['refresh_token'] = r['refresh_token']
+ writetokenfile()
+ if args.verbose:
+ print(f'NOTICE: Obtained new access token, expires {token["access_token_expiration"]}.')
+
+
+if args.authorize:
+ p = baseparams.copy()
+ p['scope'] = registration['scope']
+
+ if authflow in ('authcode', 'localhostauthcode'):
+ verifier = secrets.token_urlsafe(90)
+ challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest())[:-1]
+ redirect_uri = registration['redirect_uri']
+ listen_port = 0
+ if authflow == 'localhostauthcode':
+ # Find an available port to listen on
+ s = socket.socket()
+ s.bind(('127.0.0.1', 0))
+ listen_port = s.getsockname()[1]
+ s.close()
+ redirect_uri = 'http://localhost:'+str(listen_port)+'/'
+ # Probably should edit the port number into the actual redirect URL.
+
+ p.update({'login_hint': token['email'],
+ 'response_type': 'code',
+ 'redirect_uri': redirect_uri,
+ 'code_challenge': challenge,
+ 'code_challenge_method': 'S256'})
+ print(registration["authorize_endpoint"] + '?' +
+ urllib.parse.urlencode(p, quote_via=urllib.parse.quote))
+
+ authcode = ''
+ if authflow == 'authcode':
+ authcode = input('Visit displayed URL to retrieve authorization code. Enter '
+ 'code from server (might be in browser address bar): ')
+ else:
+ print('Visit displayed URL to authorize this application. Waiting...',
+ end='', flush=True)
+
+ class MyHandler(http.server.BaseHTTPRequestHandler):
+ '''Handles the browser query resulting from redirect to redirect_uri.'''
+
+ # pylint: disable=C0103
+ def do_HEAD(self):
+ '''Response to a HEAD requests.'''
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def do_GET(self):
+ '''For GET request, extract code parameter from URL.'''
+ # pylint: disable=W0603
+ global authcode
+ querystring = urllib.parse.urlparse(self.path).query
+ querydict = urllib.parse.parse_qs(querystring)
+ if 'code' in querydict:
+ authcode = querydict['code'][0]
+ self.do_HEAD()
+ self.wfile.write(b'<html><head><title>Authorizaton result</title></head>')
+ self.wfile.write(b'<body><p>Authorization redirect completed. You may '
+ b'close this window.</p></body></html>')
+ with http.server.HTTPServer(('127.0.0.1', listen_port), MyHandler) as httpd:
+ try:
+ httpd.handle_request()
+ except KeyboardInterrupt:
+ pass
+
+ if not authcode:
+ sys.exit('Did not obtain an authcode.')
+
+ for k in 'response_type', 'login_hint', 'code_challenge', 'code_challenge_method':
+ del p[k]
+ p.update({'grant_type': 'authorization_code',
+ 'code': authcode,
+ 'client_secret': registration['client_secret'],
+ 'code_verifier': verifier})
+ try:
+ response = urllib.request.urlopen(registration['token_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ print(err.code, err.reason)
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' in response:
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ sys.exit(1)
+
+ elif authflow == 'devicecode':
+ try:
+ response = urllib.request.urlopen(registration['devicecode_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ print(err.code, err.reason)
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' in response:
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ sys.exit(1)
+ print(response['message'])
+ del p['scope']
+ p.update({'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
+ 'client_secret': registration['client_secret'],
+ 'device_code': response['device_code']})
+ interval = int(response['interval'])
+ print('Polling...', end='', flush=True)
+ while True:
+ time.sleep(interval)
+ print('.', end='', flush=True)
+ try:
+ response = urllib.request.urlopen(registration['token_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ # Not actually always an error, might just mean "keep trying..."
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' not in response:
+ break
+ if response['error'] == 'authorization_declined':
+ print(' user declined authorization.')
+ sys.exit(1)
+ if response['error'] == 'expired_token':
+ print(' too much time has elapsed.')
+ sys.exit(1)
+ if response['error'] != 'authorization_pending':
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ sys.exit(1)
+ print()
+
+ else:
+ sys.exit(f'ERROR: Unknown OAuth2 flow "{token["authflow"]}. Delete token file and '
+ f'start over.')
+
+ update_tokens(response)
+
+
+if not access_token_valid():
+ if args.verbose:
+ print('NOTICE: Invalid or expired access token; using refresh token '
+ 'to obtain new access token.')
+ if not token['refresh_token']:
+ sys.exit('ERROR: No refresh token. Run script with "--authorize".')
+ p = baseparams.copy()
+ p.update({'client_secret': registration['client_secret'],
+ 'refresh_token': token['refresh_token'],
+ 'grant_type': 'refresh_token'})
+ try:
+ response = urllib.request.urlopen(registration['token_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ print(err.code, err.reason)
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' in response:
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ print('Perhaps refresh token invalid. Try running once with "--authorize"')
+ sys.exit(1)
+ update_tokens(response)
+
+
+if not access_token_valid():
+ sys.exit('ERROR: No valid access token. This should not be able to happen.')
+
+
+if args.verbose:
+ print('Access Token: ', end='')
+print(token['access_token'])
+
+
+def build_sasl_string(user, host, port, bearer_token):
+ '''Build appropriate SASL string, which depends on cloud server's supported SASL method.'''
+ if registration['sasl_method'] == 'OAUTHBEARER':
+ return f'n,a={user},\1host={host}\1port={port}\1auth=Bearer {bearer_token}\1\1'
+ if registration['sasl_method'] == 'XOAUTH2':
+ return f'user={user}\1auth=Bearer {bearer_token}\1\1'
+ sys.exit(f'Unknown SASL method {registration["sasl_method"]}.')
+
+
+if args.test:
+ errors = False
+
+ imap_conn = imaplib.IMAP4_SSL(registration['imap_endpoint'])
+ sasl_string = build_sasl_string(token['email'], registration['imap_endpoint'], 993,
+ token['access_token'])
+ if args.debug:
+ imap_conn.debug = 4
+ try:
+ imap_conn.authenticate(registration['sasl_method'], lambda _: sasl_string.encode())
+ # Microsoft has a bug wherein a mismatch between username and token can still report a
+ # successful login... (Try a consumer login with the token from a work/school account.)
+ # Fortunately subsequent commands fail with an error. Thus we follow AUTH with another
+ # IMAP command before reporting success.
+ imap_conn.list()
+ if args.verbose:
+ print('IMAP authentication succeeded')
+ except imaplib.IMAP4.error as e:
+ print('IMAP authentication FAILED (does your account allow IMAP?):', e)
+ errors = True
+
+ pop_conn = poplib.POP3_SSL(registration['pop_endpoint'])
+ sasl_string = build_sasl_string(token['email'], registration['pop_endpoint'], 995,
+ token['access_token'])
+ if args.debug:
+ pop_conn.set_debuglevel(2)
+ try:
+ # poplib doesn't have an auth command taking an authenticator object
+ # Microsoft requires a two-line SASL for POP
+ # pylint: disable=W0212
+ pop_conn._shortcmd('AUTH ' + registration['sasl_method'])
+ pop_conn._shortcmd(base64.standard_b64encode(sasl_string.encode()).decode())
+ if args.verbose:
+ print('POP authentication succeeded')
+ except poplib.error_proto as e:
+ print('POP authentication FAILED (does your account allow POP?):', e.args[0].decode())
+ errors = True
+
+ # SMTP_SSL would be simpler but Microsoft does not answer on port 465.
+ smtp_conn = smtplib.SMTP(registration['smtp_endpoint'], 587)
+ sasl_string = build_sasl_string(token['email'], registration['smtp_endpoint'], 587,
+ token['access_token'])
+ smtp_conn.ehlo('test')
+ smtp_conn.starttls()
+ smtp_conn.ehlo('test')
+ if args.debug:
+ smtp_conn.set_debuglevel(2)
+ try:
+ smtp_conn.auth(registration['sasl_method'], lambda _=None: sasl_string)
+ if args.verbose:
+ print('SMTP authentication succeeded')
+ except smtplib.SMTPAuthenticationError as e:
+ print('SMTP authentication FAILED:', e)
+ errors = True
+
+ if errors:
+ sys.exit(1)
diff --git a/bin/executable_ptmp b/bin/executable_ptmp
new file mode 100755
index 0000000..e42c695
--- /dev/null
+++ b/bin/executable_ptmp
@@ -0,0 +1,50 @@
+#!/bin/sh
+# Copyright 2020 Ryan Kavanagh <rak@rak.ac>
+# Upload as temporary file
+#
+# Permission to use, copy, modify, and/or distribute this software for
+# any purpose with or without fee is hereby granted, provided that the
+# above copyright notice and this permission notice appear in all
+# copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
+# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+DEST_DIR=hades.rak.ac:public_tmp
+OPT_SUFFIX=""
+
+while getopts s: f
+do
+ case $f in
+ s)
+ OPT_SUFFIX=".${OPTARG}";;
+ esac
+done
+shift `expr $OPTIND - 1`
+
+FILE="$@"
+
+if command -v md5sum >/dev/null; then
+ MD5=md5sum;
+elif command -v md5 >/dev/null; then
+ MD5="md5 -r";
+else
+ echo "No md5 found";
+fi
+
+PAD=$(${MD5} "${FILE}" | cut -f1 -d' ')
+BASE=$(basename "${FILE}")
+
+DEST_NAME="${PAD}.${BASE}${OPT_SUFFIX}"
+
+echo "${DEST_NAME}"
+
+scp "${FILE}" "${DEST_DIR}/${DEST_NAME}"
+
+echo "https://rak.ac/~tmp/${DEST_NAME}"
diff --git a/bin/executable_startaudio b/bin/executable_startaudio
new file mode 100755
index 0000000..3fbfa7f
--- /dev/null
+++ b/bin/executable_startaudio
@@ -0,0 +1,3 @@
+#!/bin/sh
+start-pulseaudio-x11
+${HOME}/bin/mpd_others.sh
diff --git a/bin/executable_tmcg b/bin/executable_tmcg
new file mode 100755
index 0000000..5ebb3cf
--- /dev/null
+++ b/bin/executable_tmcg
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+current_session () {
+ tty=$(tty)
+ for s in $(tmux list-sessions -F '#{session_name}'); do
+ tmux list-panes -F '#{pane_tty} #{session_name}' -t "$s"
+ done | grep "${tty}" | awk '{print $2}'
+}
+
+SESSION=$(current_session)
+NEW=1
+
+if [ "x${SESSION}" = "x" ]; then
+ SESSION="irc"
+ tmux new-session -s "${SESSION}" -d
+ NEW=$?
+fi
+
+if ! tmux switch -t ${SESSION}:catgirl >/dev/null 2>&1; then
+ tmux new-window -c '~' -n catgirl catgirl libera
+ tmux split-window -t ${SESSION}:catgirl -c '~' catgirl oftc
+ tmux split-window -t ${SESSION}:catgirl -c '~' catgirl sdf
+ tmux split-window -t ${SESSION}:catgirl -c '~' catgirl tilde
+ tmux set-option -t ${SESSION}:catgirl remain-on-exit on
+ tmux selectl -t ${SESSION}:catgirl tiled
+ if test ${NEW} -eq 0; then
+ tmux kill-window -t ${SESSION}:0
+ fi
+fi
diff --git a/bin/executable_wd-bak b/bin/executable_wd-bak
new file mode 100755
index 0000000..f824fba
--- /dev/null
+++ b/bin/executable_wd-bak
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+wd-mount
+
+if ! mount | grep /media/wd-bak > /dev/null ; then
+ echo "/media/wd-bak not mounted"
+ exit 1
+fi
+
+rsync -avhHP --delete-after /media/t/ /media/wd-bak
+
+sudo umount /media/wd-bak
+sudo cryptdisks_stop wd-bak
+sudo cryptdisks_stop wd-bak-work
diff --git a/bin/executable_wd-mount b/bin/executable_wd-mount
new file mode 100755
index 0000000..49466c2
--- /dev/null
+++ b/bin/executable_wd-mount
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+case `uname` in
+ OpenBSD)
+ doas bioctl -c C -l fe3fd8d53b06049b.a softraid0
+ doas mount /media/wd
+ ;;
+ Linux)
+ if ! zpool list wd-pass > /dev/null 2>&1 ; then
+ sudo zpool import -l wd-pass
+ fi
+ ;;
+ *)
+ echo "Unknown host"
+ exit 1
+esac
+
+# vim: set sw=4:
diff --git a/bin/executable_wd-umount b/bin/executable_wd-umount
new file mode 100644
index 0000000..51a11e1
--- /dev/null
+++ b/bin/executable_wd-umount
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+case `uname` in
+ OpenBSD)
+ doas umount /mnt/wd
+ doas bioctl -d 79665ba14a5187ba
+ ;;
+ Linux)
+ if zpool list wd-pass > /dev/null 2>&1; then
+ if mount -l | grep -E '^/var/lib/mpd/music '; then
+ sudo umount --lazy -f /var/lib/mpd/music
+ fi
+ mount -l -t zfs | IFS=' on ' awk '{ print $1 }' | sudo xargs -I '{}' zfs umount -u '{}'
+ sudo zpool export wd-pass
+ fi
+ ;;
+ *)
+ echo "Unknown host"
+ exit 1
+ ;;
+esac