aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/8085
-rwxr-xr-xbin/archiveit98
-rwxr-xr-xbin/assert-arg78
-rwxr-xr-xbin/backup120
-rwxr-xr-xbin/bins84
-rwxr-xr-xbin/boop83
-rwxr-xr-xbin/brightness12
-rwxr-xr-xbin/check58
-rwxr-xr-xbin/cl315
-rwxr-xr-xbin/clamp86
-rwxr-xr-xbin/color215
-rwxr-xr-xbin/copy67
-rwxr-xr-xbin/dice73
-rwxr-xr-xbin/e90
-rwxr-xr-xbin/email73
-rwxr-xr-xbin/forever68
-rwxr-xr-xbin/gc150
-rwxr-xr-xbin/gen-password73
-rwxr-xr-xbin/git-cleanup74
-rwxr-xr-xbin/grun96
-rwxr-xr-xbin/htmlesc96
-rwxr-xr-xbin/httpno156
-rwxr-xr-xbin/lc70
-rwxr-xr-xbin/li104
-rwxr-xr-xbin/lines81
-rwxr-xr-xbin/m66
-rwxr-xr-xbin/mailcfg297
-rwxr-xr-xbin/max84
-rwxr-xr-xbin/menu1587
-rwxr-xr-xbin/min85
-rwxr-xr-xbin/mkdtemp64
-rwxr-xr-xbin/mkstemp64
-rwxr-xr-xbin/msg153
-rwxr-xr-xbin/n-times73
-rwxr-xr-xbin/nato102
-rwxr-xr-xbin/ootb103
-rwxr-xr-xbin/open101
-rwxr-xr-xbin/player136
-rwxr-xr-xbin/playlist103
-rwxr-xr-xbin/pre78
-rwxr-xr-xbin/print151
-rwxr-xr-xbin/prompt76
-rwxr-xr-xbin/qr71
-rwxr-xr-xbin/repos175
-rwxr-xr-xbin/rfc150
-rwxr-xr-xbin/serve78
-rwxr-xr-xbin/slugify69
-rwxr-xr-xbin/status-bar104
-rwxr-xr-xbin/stopwatch65
-rwxr-xr-xbin/tmp91
-rwxr-xr-xbin/tmpname66
-rwxr-xr-xbin/tuivid65
-rwxr-xr-xbin/uc70
-rwxr-xr-xbin/untill83
-rwxr-xr-xbin/update73
-rwxr-xr-xbin/upgrade66
-rwxr-xr-xbin/uri75
-rwxr-xr-xbin/uuid62
-rwxr-xr-xbin/vcs255
-rwxr-xr-xbin/vm176
-rwxr-xr-xbin/volume112
-rwxr-xr-xbin/with-email91
-rwxr-xr-xbin/without-env70
-rwxr-xr-xbin/wms95
-rwxr-xr-xbin/x124
l---------bin/xdg-open1
-rwxr-xr-xbin/xmpp99
-rwxr-xr-xbin/yt113
-rwxr-xr-xbin/z153
69 files changed, 8480 insertions, 0 deletions
diff --git a/bin/80 b/bin/80
new file mode 100755
index 0000000..b971f5d
--- /dev/null
+++ b/bin/80
@@ -0,0 +1,85 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ 80 [FILENAME...]
+ 80 -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ FILENAME the name of the file to work on (default:
+ the list of file in the VCS repository)
+
+
+ List the lines in the files that contain more than 80 columns.
+
+
+ Examples:
+
+ Check for all the files in the current repository:
+
+ $ 80
+
+
+ Detect on the given two files:
+
+ $ 80 f1 f2
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+len() {
+ awk '
+ length > 80 {
+ printf "%s:%s:%s\n", FILENAME, FNR, $0
+ }
+ ' "$1"
+}
+
+if [ $# = 0 ]; then
+ vcs ls-files | while read -r f; do
+ len "$f"
+ done
+else
+ for f in "$@"; do
+ len "$f"
+ done
+fi
diff --git a/bin/archiveit b/bin/archiveit
new file mode 100755
index 0000000..da132d7
--- /dev/null
+++ b/bin/archiveit
@@ -0,0 +1,98 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ archiveit
+ archiveit -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Grab all bookmarks from Firefox that contains tags and archive
+ them with ArchiveBox.
+
+
+ Examples:
+
+ Just run it:
+
+ $ archiveit
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+# Derived from ArchiveBox:
+# https://github.com/pirate/ArchiveBox/blob/db1f6efc934bbcdf53377bf51a064c6fd0fc5b1d/bin/archivebox-export-browser-history#L23-L37
+QUERY="$(
+ cat <<-'EOF'
+ SELECT json_object('timestamp', dateAdded, 'description', title, 'href', url)
+ FROM (
+ SELECT b.dateAdded, b.title, p.url
+ FROM moz_bookmarks AS b
+ JOIN moz_places AS p
+ ON b.fk = p.id
+ WHERE b.fk IN (
+ SELECT DISTINCT(fk) FROM moz_bookmarks
+ WHERE parent IN (
+ -- get all tags
+ SELECT id FROM moz_bookmarks
+ WHERE parent = 4
+ )
+ )
+ AND b.title IS NOT NULL
+ ORDER BY
+ b.dateAdded ASC,
+ b.title ASC,
+ p.url
+ )
+ EOF
+)"
+
+# Copy the database because it is locked.
+DB="$(mkstemp)"
+cp ~/.mozilla/firefox/*.default/places.sqlite "$DB"
+
+cd ~/Documents/Archive/
+
+sqlite3 "$DB" "$QUERY" | archivebox add
+archivebox update
diff --git a/bin/assert-arg b/bin/assert-arg
new file mode 100755
index 0000000..d7bc8f4
--- /dev/null
+++ b/bin/assert-arg
@@ -0,0 +1,78 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ assert-arg STRING MESSAGE
+ assert-arg -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ STRING the string to check if is empty
+ MESSAGE the message to print when STRING is empty
+
+
+ Examples:
+
+ Assert that $1 contains an argument, named FILENAME:
+
+ $ eval "$(assert-arg "${1:-}" 'FILENAME')"
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+STRING="${1:-}"
+MESSAGE="${2:-}"
+
+if [ -z "$MESSAGE" ]; then
+ printf 'Missing MESSAGE, an argument to assert-arg.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+if [ -z "$STRING" ]; then
+ printf 'Missing %s.\n\n' "$MESSAGE" >&2
+ cat <<-'EOF'
+ usage >&2
+ exit 2
+ EOF
+fi
diff --git a/bin/backup b/bin/backup
new file mode 100755
index 0000000..be7d996
--- /dev/null
+++ b/bin/backup
@@ -0,0 +1,120 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ backup [-q] [-C COMMENT] [ARCHIVE_TAG]
+ backup -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -q disable verbose mode, useful for
+ non-interactive sessions
+ -C COMMENT the comment text to be attached to the archive
+ -h, --help show this message
+
+ ARCHIVE_TAG the tag used to create the new
+ backup (default: "manual")
+
+
+ The repository is expected to have been created with:
+
+ $ borg init -e repokey-blake2
+
+ The following environment variables are expected to be exported:
+
+ $BORG_PASSCOMMAND
+ $BORG_REPO
+ $BORG_REMOTE_PATH
+
+ Password-less SSH access is required, usually done via adding
+ ~/.ssh/id_rsa.pub to suyin:.ssh/authorized_keys.
+
+
+ Examples:
+
+ Run backup manually:
+
+ $ backup
+
+ Create backup with comment, and verbose mode active:
+
+ $ backup -qC 'The backup has a comment' my-backup
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+VERBOSE_FLAGS='--verbose --progress'
+COMMENT=''
+while getopts 'qC:h' flag; do
+ case "$flag" in
+ q)
+ VERBOSE_FLAGS=''
+ ;;
+ C)
+ COMMENT="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+ARCHIVE_TAG="${1:-manual}"
+
+
+run() {
+ set -x
+ # The contents of $VERBOSE_FLAGS doesn't involve user input:
+ # shellcheck disable=2086
+ borg create \
+ $VERBOSE_FLAGS \
+ --comment "$COMMENT" \
+ --exclude "$XDG_CACHE_HOME" \
+ --exclude ~/Downloads/ \
+ --stats \
+ --compression lzma,9 \
+ "::{hostname}-{now}-$ARCHIVE_TAG" \
+ ~/
+ STATUS=$?
+ set +x
+
+ if [ "$STATUS" = 0 ]; then
+ return 0
+ elif [ "$STATUS" = 1 ]; then
+ printf 'WARNING, but no ERROR.\n' >&2
+ return 0
+ else
+ return "$STATUS"
+ fi
+}
+
+run || exit $?
diff --git a/bin/bins b/bin/bins
new file mode 100755
index 0000000..9f869de
--- /dev/null
+++ b/bin/bins
@@ -0,0 +1,84 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ bins [-F]
+ bins -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -F force remove the cache file
+ -h, --help show this message
+
+
+ List the available binaries in $PATH. The result is cached on
+ the $XDG_CACHE_HOME/euandreh/bins file if $PATH values didn't
+ change.
+
+
+ Examples:
+
+ Pick an executable using `dmenu`:
+
+ $ bins | dmenu
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+FORCE=false
+while getopts 'Fh' flag; do
+ case "$flag" in
+ F)
+ FORCE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND- 1))
+
+
+F="$XDG_CACHE_HOME/euandreh/bins"
+if [ "$FORCE" = true ]; then
+ rm -f "$F"
+fi
+
+IFS=:
+# Word-splitting is the goal:
+# shellcheck disable=2086
+if stest -rdq -n "$F" $PATH; then
+ T="$(mkstemp)"
+ trap 'rm -f $T' EXIT
+ stest -lxf $PATH | sort -u > "$T"
+ mv "$T" "$F"
+fi
+
+cat "$F"
diff --git a/bin/boop b/bin/boop
new file mode 100755
index 0000000..a7792b3
--- /dev/null
+++ b/bin/boop
@@ -0,0 +1,83 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ boop [-m MESSAGE] -- COMMAND...
+ boop -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -m MESSAGE text message of the desktop
+ notification (default: COMMAND)
+ -h, --help show this message
+
+ COMMAND the commands to be executed
+
+
+ Examples:
+
+ Play the positive sound, using the command as message:
+
+ $ boop echo 123
+
+ Fail with the underlying 127 return code with the
+ message "ERROR":
+
+ $ boop -m ERROR ech 123
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'm:h' flag; do
+ case "$flag" in
+ m)
+ MESSAGE="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+set +e
+"$@"
+STATUS=$?
+set -e
+
+if [ "$STATUS" = 0 ]; then
+ N=0
+else
+ N=1
+fi
+
+CMD="$*"
+msg -"$N" -bs -D "${MESSAGE:-$CMD}"
+
+exit "$STATUS"
diff --git a/bin/brightness b/bin/brightness
new file mode 100755
index 0000000..10bd198
--- /dev/null
+++ b/bin/brightness
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+BRIGHTNESS_DIFF="$1"
+
+HANDLER=/sys/class/backlight/"$BACKLIGHT_DEVICE"
+
+OLD_BRIGHTNESS="$(cat "$HANDLER"/brightness)"
+MAX_BRIGHTNESS="$(cat "$HANDLER"/max_brightness)"
+SUM=$((OLD_BRIGHTNESS + BRIGHTNESS_DIFF))
+NEW_BRIGHTNESS="$(clamp -- "$SUM" 0 "$MAX_BRIGHTNESS")"
+echo "$NEW_BRIGHTNESS" > "$HANDLER"/brightness || sudo chmod 666 "$HANDLER"/brightness
diff --git a/bin/check b/bin/check
new file mode 100755
index 0000000..8348041
--- /dev/null
+++ b/bin/check
@@ -0,0 +1,58 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ check
+ check -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Run Makefile tests. This binary is available to simplify the
+ cronjob.
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+cd "$XDG_PREFIX"
+make check
diff --git a/bin/cl b/bin/cl
new file mode 100755
index 0000000..87d7c92
--- /dev/null
+++ b/bin/cl
@@ -0,0 +1,315 @@
+#!/bin/sh
+set -eu
+
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+tmpname() {
+ echo "${TMPDIR:-/tmp}/cl.tmpfile.$(uuid)"
+}
+
+mkstemp() {
+ name="$(tmpname)"
+ touch "$name"
+ echo "$name"
+}
+
+escape_name() {
+ printf '%s' "$1" |
+ sed 's|"|\\"|g' |
+ printf '(load "%s")\n' "$(cat -)"
+}
+
+
+IMPLEMENTATIONS='
+abcl
+allegro
+clasp
+clisp
+clozure
+cmucl
+ecl
+jscl
+mkcl
+sbcl
+'
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ cl [-e EXP] [-f FILE] [-p] [-M IMAGE] [-I IMPL] [-n] [-v] [FILE...] [-- LISP_OPTIONS]
+ cl -l
+ cl -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -e EXP an sexp to be evaluated (can be given more than once)
+ -E EXP an sexp to be executed as a script
+ -f FILE a file to be evaluated (can be given more than once)
+ -p print the value of the last given expression
+ -M IMAGE load the given Lisp image
+ -I IMPL use the given implementation (default: $LISP_CLI_IMPL)
+ -n skip loading the implementation's init file
+ -v verbose mode
+ -l list the known types of implementations
+ -h, --help show this message
+
+ FILE the file to be executed as a script
+ LISP_OPTIONS options to be forwarrded to the underlying Lisp command
+
+
+ Lauch the desired Lisp implementation, properly adapting the given
+ CLI options.
+
+ When the implementation is not explicited on the command line, and
+ the $LISP_CLI_IMPL environment variable in unset, implementations are
+ searched for alphabetically in $PATH, untill one is found, otherwise
+ an error is emitted.
+
+ The supported implementations are:
+ EOF
+
+
+ for i in $IMPLEMENTATIONS; do
+ printf -- '- %s\n' "$i"
+ done
+
+ cat <<-'EOF'
+
+
+ Examples:
+
+ Launch CLISP REPL, when $LISP_CLI_IMPL is set to 'clisp':
+
+ $ cl
+
+
+ Lauch SBCL REPL, with the given Lisp image loaded, skipping the
+ loading of the '.sbclrc' file:
+
+ $ cl -n -Isbcl sbcl.image
+
+
+ Run file1.lisp with ABCL:
+
+ $ cl -Iabcl file1.lisp
+
+
+ Process STDIN:
+
+ $ cat <<-EOS > process.lisp
+
+ EOS
+ $ cat f1.txt f2.txt | cl -p process.lisp
+
+
+ Print a value on all types of implementations:
+
+ $ for i in `cl -l`; do cl -I$i -pe 'call-arguments-list'; done
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+NO_RC=false
+SCRIPT="$(mkstemp)"
+LISP_CLI_RC="${XDG_CONFIG_HOME:-$HOME/.config}/lisp-cli/init.lisp"
+VERBOSE=false
+IMAGE=''
+IMPL="${LISP_CLI_IMPL:-}"
+INTERACTIVE=true
+while getopts 'e:E:f:pM:I:nvlh' flag; do
+ case "$flag" in
+ e)
+ printf '%s\n' "$OPTARG" >> "$SCRIPT"
+ ;;
+ E)
+ printf '%s\n' "$OPTARG" >> "$SCRIPT"
+ INTERACTIVE=false
+ ;;
+ f)
+ escape_name "$OPTARG" >> "$SCRIPT"
+ ;;
+ M)
+ IMAGE="$OPTARG"
+ ;;
+ I)
+ IMPL="$OPTARG"
+ ;;
+ n)
+ NO_RC=true
+ ;;
+ v)
+ VERBOSE=true
+ ;;
+ l)
+ for i in $IMPLEMENTATIONS; do
+ printf '%s\n' "$i"
+ done
+ exit
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+shift $((OPTIND - 2))
+if [ "$1" != '--' ]; then
+ shift
+fi
+
+PRESERVE_ARGS=false
+for f in "$@"; do
+ if [ "$f" = '--' ]; then
+ PRESERVE_ARGS=true
+ shift
+ break
+ fi
+ INTERACTIVE=false
+ escape_name "$f" >> "$SCRIPT"
+done
+
+if [ "$PRESERVE_ARGS" = false ]; then
+ set --
+fi
+
+MAIN="$(mkstemp)"
+if [ "$NO_RC" = false ] && [ -e "$LISP_CLI_RC" ]; then
+ escape_name "$LISP_CLI_RC" > "$MAIN"
+fi
+
+if [ "$INTERACTIVE" = true ]; then
+ escape_name "$SCRIPT" >> "$MAIN"
+else
+ cat <<-EOF >> "$MAIN"
+ (handler-case
+ (progn
+ (load "$SCRIPT"
+ :verbose nil
+ :print nil)
+ (uiop:quit 0))
+ (error (e)
+ (format *error-output* "~&~%error: ~a~%" e)
+ (uiop:quit 1)))
+ EOF
+fi
+
+if [ -z "$IMPL" ]; then
+ for i in $IMPLEMENTATIONS; do
+ if command -v "$i" > /dev/null; then
+ IMPL="$i"
+ break
+ fi
+ done
+ if [ -z "$IMPL" ]; then
+ printf "Could not find any implementation in \$PATH.\n" >&2
+ exit 2
+ fi
+fi
+
+
+case "$IMPL" in
+ abcl)
+ exit 4
+ ;;
+ allegro)
+ exit 4
+ ;;
+ clasp)
+ exit 4
+ ;;
+ clisp)
+ set -- -ansi -i "$MAIN" "$@"
+ if [ -n "$IMAGE" ]; then
+ set -- -M "$IMAGE" "$@"
+ fi
+ if [ "$NO_RC" = true ]; then
+ set -- -norc "$@"
+ fi
+ if [ "$VERBOSE" = false ]; then
+ set -- -q -q "$@"
+ else
+ set -x
+ fi
+ exec clisp "$@"
+ ;;
+ clozure)
+ set -- -l "$MAIN" "$@"
+ if [ -n "$IMAGE" ]; then
+ set -- -I "$IMAGE" "$@"
+ fi
+ if [ "$NO_RC" = true ]; then
+ set -- -n "$@"
+ fi
+ if [ "$VERBOSE" = false ]; then
+ set -- -Q "$@"
+ else
+ set -x
+ fi
+ exec ccl "$@"
+ ;;
+ cmucl)
+ exit 4
+ ;;
+ ecl)
+ exit 4
+ ;;
+ jscl)
+ exit 4
+ ;;
+ mkcl)
+ exit 4
+ ;;
+ sbcl)
+ set -- --load "$MAIN" "$@"
+ if [ -n "$IMAGE" ]; then
+ # The '--core' "C runtime option" must appear before the
+ # other "Lisp options", such as '--load'.
+ set -- --core "$IMAGE" "$@"
+ fi
+ if [ "$NO_RC" = true ]; then
+ set -- --no-sysinit --no-userinit "$@"
+ fi
+ if [ "$VERBOSE" = false ]; then
+ set -- --noinform "$@"
+ else
+ set -x
+ fi
+ exec sbcl "$@"
+ ;;
+ *)
+ printf 'Unsupported implementation: "%s".\n\n' "$IMPL" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/clamp b/bin/clamp
new file mode 100755
index 0000000..a673f72
--- /dev/null
+++ b/bin/clamp
@@ -0,0 +1,86 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ clamp NUMBER MIN MAX
+ clamp -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Clamp the NUMBER between MIN and MAX.
+
+
+ Examples:
+
+ Assert the number is within the interval:
+
+ $ clamp 5 3 9
+ 5
+
+ When a number is below MIN it gets clamped:
+
+ $ clamp 1 3 9
+ 3
+
+ When a number is above MAX it gets clamped:
+
+ $ clamp 15 3 9
+ 9
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+NUMBER="${1:-}"
+MIN="${2:-}"
+MAX="${3:-}"
+
+eval "$(assert-arg "$NUMBER" 'NUMBER')"
+eval "$(assert-arg "$MIN" 'MIN')"
+eval "$(assert-arg "$MAX" 'MAX')"
+
+
+if [ "$MIN" -gt "$MAX" ]; then
+ printf 'MIN (%s) is greater then MAX (%s).\n' "$MIN" "$MAX" >&2
+ exit 2
+fi
+
+min -- "$(max -- "$NUMBER" "$MIN")" "$MAX"
diff --git a/bin/color b/bin/color
new file mode 100755
index 0000000..dc610e9
--- /dev/null
+++ b/bin/color
@@ -0,0 +1,215 @@
+#!/bin/sh
+# shellcheck disable=2059
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ color -c COLOR TEXT
+ color -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -c COLOR
+ -h, --help show this message
+
+
+ Print the given text with a color.
+
+ The available colors are:
+ EOF
+ list_colors | sed 's/^/ /'
+
+ cat <<-'EOF'
+
+ Examples:
+
+ Print "banana" in yellow:
+
+ $ color -c yellow 'banana'
+ banana
+
+ Print "grass" in green, with a newline:
+
+ $ color -c green 'grass\n'
+ grass
+ EOF
+}
+
+
+END="\033[0m"
+
+black() {
+ BLACK="\033[0;30m"
+ printf "${BLACK}${1}${END}"
+}
+
+blackb() {
+ BLACK_B="\033[1;30m"
+ printf "${BLACK_B}${1}${END}"
+}
+
+blacki() {
+ BLACK_I="\033[0;90m"
+ printf "${BLACK_I}${1}${END}"
+}
+
+white() {
+ WHITE="\033[0;37m"
+ printf "${WHITE}${1}${END}"
+}
+
+whiteb() {
+ WHITE_B="\033[1;37m"
+ printf "${WHITE_B}${1}${END}"
+}
+
+red() {
+ RED="\033[0;31m"
+ printf "${RED}${1}${END}"
+}
+
+redb() {
+ RED_B="\033[1;31m"
+ printf "${RED_B}${1}${END}"
+}
+
+green() {
+ GREEN="\033[0;32m"
+ printf "${GREEN}${1}${END}"
+}
+
+greenb() {
+ GREEN_B="\033[1;32m"
+ printf "${GREEN_B}${1}${END}"
+}
+
+yellow() {
+ YELLOW="\033[0;33m"
+ printf "${YELLOW}${1}${END}"
+}
+
+yellowb() {
+ YELLOW_B="\033[1;33m"
+ printf "${YELLOW_B}${1}${END}"
+}
+
+blue() {
+ BLUE="\033[0;34m"
+ printf "${BLUE}${1}${END}"
+}
+
+blueb() {
+ BLUE_B="\033[1;34m"
+ printf "${BLUE_B}${1}${END}"
+}
+
+bluei() {
+ BLUE_I="\033[0;94m"
+ printf "${BLUE_I}${1}${END}"
+}
+
+purple() {
+ PURPLE="\033[0;35m"
+ printf "${PURPLE}${1}${END}"
+}
+
+
+purpleb() {
+ PURPLE_B="\033[1;35m"
+ printf "${PURPLE_B}${1}${END}"
+}
+
+lightblue() {
+ LIGHT_BLUE="\033[0;36m"
+ printf "${LIGHT_BLUE}${1}${END}"
+}
+
+lightblueb() {
+ LIGHT_BLUE_B="\033[1;36m"
+ printf "${LIGHT_BLUE_B}${1}${END}"
+}
+
+COLOR_LIST='
+black
+blackb
+white
+whiteb
+red
+redb
+green
+greenb
+yellow
+yellowb
+blue
+blueb
+purple
+purpleb
+lightblue
+lightblueb
+blacki
+bluei
+'
+list_colors() {
+ for c in $COLOR_LIST; do
+ printf '%s\n' "$("$c" "$c")"
+ done
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+COLOR_FN=''
+while getopts 'c:h' flag; do
+ case "$flag" in
+ c)
+ EXISTS=false
+ for c in $COLOR_LIST; do
+ if [ "$OPTARG" = "$c" ]; then
+ EXISTS=true
+ break
+ fi
+ done
+ if [ "$EXISTS" = false ]; then
+ printf 'Invalid color: %s\n' "$OPTARG" >&2
+ exit 2
+ fi
+ COLOR_FN="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+TEXT="${1:-}"
+
+eval "$(assert-arg "$COLOR_FN" '-c COLOR')"
+eval "$(assert-arg "$TEXT" 'TEXT')"
+
+
+"$COLOR_FN" "$TEXT"
diff --git a/bin/copy b/bin/copy
new file mode 100755
index 0000000..64e1e32
--- /dev/null
+++ b/bin/copy
@@ -0,0 +1,67 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ copy [-n] < STDIN
+ copy -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -n remove newlines
+ -h, --help show this message
+
+ Examples:
+
+ Copy numbers to clipboard:
+ seq 10 | copy
+
+ Copy string without newline:
+ echo 'with automatic newline' | copy -n
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+TRIM=false
+while getopts 'nh' flag; do
+ case "$flag" in
+ n)
+ TRIM=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ "$TRIM" = true ]; then
+ cat - | tr -d '\n' | xclip -sel clip
+else
+ cat - | xclip -sel clip
+fi
diff --git a/bin/dice b/bin/dice
new file mode 100755
index 0000000..4a145d0
--- /dev/null
+++ b/bin/dice
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ dice [SIZE]
+ dice -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ SIZE the size of the dice (default: 6)
+
+
+ Roll a dice of SIZE. Caveat: rolling a dice more than once in
+ the same second will give you the same number.
+
+
+ Examples:
+
+ Roll a dice of size 6:
+
+ $ dice
+ 3
+
+ Roll a D20:
+
+ $ dice 20
+ 15
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+SIZE="${1:-6}"
+RAND="$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null)"
+echo $(((RAND % SIZE) + 1))
diff --git a/bin/e b/bin/e
new file mode 100755
index 0000000..d0b261a
--- /dev/null
+++ b/bin/e
@@ -0,0 +1,90 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ e [FILE]
+ e -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help
+
+
+ Flexibly run a text editor, either directly on in a pipe.
+
+ Examples:
+
+ Edit "file.txt":
+
+ $ e file.txt
+
+ Manipulate the content of a pipe midway:
+
+ $ seq 10 | e | grep 5
+
+ The editor used is either $VISUAL or $EDITOR, with a fallback to
+ vi in case any of those variables aren't defined.
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+CMD="${VISUAL:-${EDITOR:-vi}}"
+if [ "$CMD" = 'e' ]; then
+ CMD='vi'
+fi
+
+if [ ! -t 0 ]; then
+ F="$(mkstemp)"
+ cat > "$F"
+ exec 0</dev/tty
+ exec 3>&1
+ exec 1>/dev/tty
+ $CMD "$F"
+ cat "$F" >&3
+else
+ if [ $# -eq 0 ]; then
+ f="$(fzf --select-1 --exit-0 < "$XDG_DATA_HOME"/euandreh/e.list.txt)"
+ if [ -n "$f" ]; then
+ history -s e "$f"
+ sh -c "$CMD $f"
+ fi
+ else
+ $CMD "$@"
+ fi
+fi
diff --git a/bin/email b/bin/email
new file mode 100755
index 0000000..87d1cec
--- /dev/null
+++ b/bin/email
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ email -s SUBJECT ADDRESS... < BODY
+ email -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -s SUBJECT the email subject
+ -h, --help show this message
+
+ ADDRESS the email addresses to send the email to
+ BODY the text to be sent as the body
+
+
+ Send an email to ADDRESS using BODY.
+
+
+ Examples:
+
+ Send 10 numbers to mail@example.com:
+
+ $ seq 10 | email -s number mail@email.com
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 's:h' flag; do
+ case "$flag" in
+ s)
+ SUBJECT="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+eval "$(assert-arg "${SUBJECT:-}" '-s SUBJECT')"
+eval "$(assert-arg "${1:-}" 'ADDRESS')"
+
+printf 'Content-Type: text/plain; charset=UTF-8\nSubject: %s\n\n%s' \
+ "$(echo "$SUBJECT" | tr -d '\n')" \
+ "$(cat)" |
+ msmtpq -a EuAndreh "$@"
diff --git a/bin/forever b/bin/forever
new file mode 100755
index 0000000..d4410e5
--- /dev/null
+++ b/bin/forever
@@ -0,0 +1,68 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ forever -- COMMAND...
+ forever -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Run COMMAND forever.
+
+
+ Examples:
+
+ Print 123 forever:
+
+ $ forever echo 123
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+eval "$(assert-arg "${1:-}" 'COMMAND')"
+
+
+while true; do
+ STATUS=0
+ "$@" || STATUS=$?
+ printf 'Exitted with code %s.\n' "$STATUS" >&2
+done
diff --git a/bin/gc b/bin/gc
new file mode 100755
index 0000000..60a6376
--- /dev/null
+++ b/bin/gc
@@ -0,0 +1,150 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ gc [TYPE]
+ gc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ TYPE what to do GC on (default: all):
+ - guix
+ - nohup
+ - trash
+ - tmpdir
+ - docker
+ - email
+ - vcs
+
+
+ Free disk space system-wide.
+
+
+ Examples:
+
+ Just run it, for all:
+
+ $ gc
+
+
+ Cleanup tmpdir:
+
+ $ gc tmpdir
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+disk() {
+ df -h . |
+ awk 'NR == 2 { printf "%s - %s/%s\n", $4, $3, $2 }'
+}
+
+
+gc_guix() {
+ pass show velhinho/0-andreh-password | head -n1 | sudo -iS guix system delete-generations
+ pass show velhinho/0-andreh-password | head -n1 | sudo -iS guix gc --optimize -d
+ guix home delete-generations
+ guix gc --optimize -d
+}
+
+gc_nohup() {
+ find ~/ -type f -name 'nohup.out' -exec rm -vf "{}" \;
+}
+
+gc_trash() {
+ yes | trash-empty
+}
+
+gc_tmpdir() {
+ find "${TMPDIR:-/tmp}" -type f -atime +10 -exec rm -vf "{}" \; ||:
+}
+
+gc_docker() {
+ if command -v docker; then
+ yes | docker system prune -a
+ docker rmi "$(docker images -a -q)" ||:
+ docker rm "$(docker ps -a -f status=exited -q)" ||:
+ docker stop "$(docker ps -a -q)" ||:
+ docker rm "$(docker ps -a -q)" ||:
+ yes | docker volume prune
+ yes | docker container prune
+ fi
+}
+
+gc_email() {
+ notmuch search --output=files --exclude=false tag:killed |
+ xargs -I{} rm -vf "{}"
+}
+
+gc_vcs() {
+ repos -e ~/dev/go/ -e ~/dev/quicklisp/ -e ~/dev/archive/ ~/dev/ |
+ xargs -I% -P4 x vcs -C% gc OR true
+}
+
+
+gc_all() {
+ set -x
+ gc_guix
+ gc_nohup
+ gc_trash
+ gc_tmpdir
+ gc_docker
+ gc_email
+ gc_vcs
+ set +x
+}
+
+TYPE="${1:-all}"
+CMD=gc_"$TYPE"
+if ! command -v "$CMD" >/dev/null; then
+ printf 'Invalid TYPE: "%s".\n\n' "$TYPE" >&2
+ usage >&2
+ exit 2
+fi
+
+
+BEFORE="$(disk)"
+"$CMD"
+AFTER="$(disk)"
+
+printf 'Disk space:\n'
+printf ' before: %s\n' "$BEFORE"
+printf ' after: %s\n' "$AFTER"
diff --git a/bin/gen-password b/bin/gen-password
new file mode 100755
index 0000000..327858f
--- /dev/null
+++ b/bin/gen-password
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ gen-password [LENGTH]
+ gen-password -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ LENGTH the length of the generated password
+ (default: 999)
+
+
+ Generate a random password, and emit it to STDOUT.
+
+
+ Examples:
+
+ Create a file with a secret:
+
+ $ gen-password > secret.txt
+
+
+ Generate an absurdly long secret:
+
+ $ gen-password 9999 > giant-secret.txt
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+LENGTH="${1:-999}"
+
+tr -cd '[:alnum:]' < /dev/random |
+ fold -w "$LENGTH" |
+ head -n1
diff --git a/bin/git-cleanup b/bin/git-cleanup
new file mode 100755
index 0000000..4196cff
--- /dev/null
+++ b/bin/git-cleanup
@@ -0,0 +1,74 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ git cleanup [REMOTE]
+ git cleanup -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ REMOTE the remote to prune the remote tracking
+ branches from (default: origin)
+
+
+ Delete merged branches, both local and remote-tracking.
+
+
+ Examples:
+
+ Cleanup branches from "origin":
+
+ $ git cleanup
+
+
+ Delete branches from "upstream":
+
+ $ git cleanup upstream
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+REMOTE="${1:-origin}"
+
+git branch --merged |
+ grep -v -e '^\*' -e '^. main$' |
+ xargs git branch -d
+
+git remote prune "$REMOTE"
diff --git a/bin/grun b/bin/grun
new file mode 100755
index 0000000..74d8819
--- /dev/null
+++ b/bin/grun
@@ -0,0 +1,96 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ grun [-r RECIPIENT] FILENAME -- COMMAND...
+ grun -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -r RECIPIENT the recipient to encrypt to. Can be provided
+ multiple times for multiple recipients.
+ -h, --help show this message
+
+ COMMAND A command to be executed, that accepts input
+ in STDIN and emits result in STDOUT, and emits
+ errors as non-zero return codes.
+ FILENAME The GPG-encrypted file to be processed. If it
+ doesn't exist yet, it will be created.
+
+ Examples:
+
+ Edit "secrets.txt.gpg" using `vipe` and the default recipient:
+
+ $ grun secrets.txt.gpg -- vipe
+
+ Delete lines containing "TODO" in todos.gpg for specific keys:
+
+ $ grun -r ABC123DEF321 todos.gpg -- sed '/TODO/d'
+
+ If COMMAND emits a non-zero return code, the file is left
+ unmodified.
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'rh' flag; do
+ case "$flag" in
+ r)
+ RECIPIENTS_FLAG="${RECIPIENTS_FLAG:-} -r $OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+FILENAME="${1:-}"
+eval "$(assert-arg "$FILENAME" 'FILENAME')"
+shift
+
+if [ "${1:-}" != '--' ]; then
+ printf 'Missing "--" separator\n\n' >&2
+ usage >&2
+ exit 2
+fi
+shift
+
+eval "$(assert-arg "${1:-}" 'COMMAND')"
+
+
+if [ ! -e "$FILENAME" ]; then
+ OUT="$(printf '' | "$@")"
+else
+ OUT="$(gpg -dq "$FILENAME" | "$@")"
+fi
+
+# GPG recipients can't contain spaces:
+# shellcheck disable=2086
+echo "$OUT" | gpg -e ${RECIPIENTS_FLAG:--r eu@euandre.org} | sponge "$FILENAME"
diff --git a/bin/htmlesc b/bin/htmlesc
new file mode 100755
index 0000000..d9c59bd
--- /dev/null
+++ b/bin/htmlesc
@@ -0,0 +1,96 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ htmlesc [-e|d]
+ htmlesc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -e escape the string (the default action)
+ -d unescape (de-escape?) the string
+ -h, --help show this message
+
+
+ Get a string from STDIN and convert it to/from HTML escaping.
+
+
+ Examples:
+
+ oij
+
+ $ printf 'a > 5 && !b' | htmlesc
+ a &gt; 5 &amp;&amp; !b
+
+
+ Unescape the content from a file:
+
+ $ htmlesc -d < file.html
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+ENCODE=false
+DECODE=false
+while getopts 'edh' flag; do
+ case "$flag" in
+ e)
+ ENCODE=true
+ ;;
+ d)
+ DECODE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ "$ENCODE" = true ] && [ "$DECODE" = true ]; then
+ printf 'Both -e and -d given. Pick one.\n' >&2
+ usage >&2
+ exit 2
+elif [ "$DECODE" = true ]; then
+ sed \
+ -e 's|&amp;|\&|g' \
+ -e 's|&lt;|<|g' \
+ -e 's|&gt;|>|g' \
+ -e 's|&quot;|"|g' \
+ -e "s|&#39;|'|g"
+else
+ sed \
+ -e 's|&|\&amp;|g' \
+ -e 's|<|\&lt;|g' \
+ -e 's|>|\&gt;|g' \
+ -e 's|"|\&quot;|g' \
+ -e "s|'|\&#39;|g"
+fi
diff --git a/bin/httpno b/bin/httpno
new file mode 100755
index 0000000..e64b872
--- /dev/null
+++ b/bin/httpno
@@ -0,0 +1,156 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ httpno [NUMBER|TEXT]
+ httpno -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ NUMBER the number of the HTTP status code
+ TEXT the text of the description of the status code
+
+
+ Print the given status code, or list them all when no arguments
+ are given.
+
+
+ Examples:
+
+ Get the status code 404:
+
+ $ httpno 404
+ 404 Not Found
+
+
+ Get the status code for "created":
+
+ $ httpno created
+ 201 Created
+
+
+ List all statuses:
+
+ $ httpno
+ ...
+ EOF
+}
+
+DATA() {
+ awk 'd == 1 { print; next } /^__DATA__$/ { d = 1 }' "$0" |
+ head -n -1 # trim ShellCheck quote
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ $# -eq 0 ]; then
+ DATA
+else
+ DATA | grep -i "$@"
+fi
+
+
+exit
+
+# Make ShellCheck happy. See https://github.com/koalaman/shellcheck/issues/1201
+# shellcheck disable=1112
+echo "
+__DATA__
+100 Continue
+101 Switching Protocols
+102 Processing
+200 OK
+201 Created
+202 Accepted
+203 Non-Authoritative Information
+204 No Content
+205 Reset Content
+206 Partial Content
+207 Multi-Status
+208 Already Reported
+300 Multiple Choices
+301 Moved Permanently
+302 Found
+303 See Other
+304 Not Modified
+305 Use Proxy
+307 Temporary Redirect
+400 Bad Request
+401 Unauthorized
+402 Payment Required
+403 Forbidden
+404 Not Found
+405 Method Not Allowed
+406 Not Acceptable
+407 Proxy Authentication Required
+408 Request Timeout
+409 Conflict
+410 Gone
+411 Length Required
+412 Precondition Failed
+413 Request Entity Too Large
+414 Request-URI Too Large
+415 Unsupported Media Type
+416 Request Range Not Satisfiable
+417 Expectation Failed
+418 I'm a teapot
+420 Blaze it
+422 Unprocessable Entity
+423 Locked
+424 Failed Dependency
+425 No code
+426 Upgrade Required
+428 Precondition Required
+429 Too Many Requests
+431 Request Header Fields Too Large
+449 Retry with
+500 Internal Server Error
+501 Not Implemented
+502 Bad Gateway
+503 Service Unavailable
+504 Gateway Timeout
+505 HTTP Version Not Supported
+506 Variant Also Negotiates
+507 Insufficient Storage
+509 Bandwidth Limit Exceeded
+510 Not Extended
+511 Network Authentication Required
+"
diff --git a/bin/lc b/bin/lc
new file mode 100755
index 0000000..111b91b
--- /dev/null
+++ b/bin/lc
@@ -0,0 +1,70 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ lc
+ lc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Transforms text from STDIN from upper-case to lower-case. It is
+ the equivalent of running 'tr [:upper:] [:lower:]'.
+
+
+ Examples:
+
+ Normalize to lower-case:
+
+ $ echo EuAndreh | lc
+ euandreh
+
+
+ Keep the text as-is:
+
+ $ echo andreh | lc
+ andreh
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+tr '[:upper:]' '[:lower:]'
diff --git a/bin/li b/bin/li
new file mode 100755
index 0000000..1e7917f
--- /dev/null
+++ b/bin/li
@@ -0,0 +1,104 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ li [-I IMPL ] [-v] [OPTIONS...]
+ li -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -I IMPL use the given implementation (default: $LISP_CLI_IMPL)
+ -v verbose mode
+ -h, --help show this message
+
+ OPTIONS options to be forwarded to cl(1) (lisp-cli)
+
+
+ Run the cl(1) executable with OPTIONS, but make sure an up-to-date
+ image exists and load it.
+
+
+ Examples:
+
+ Launch a REPL from an image:
+
+ $ li
+
+
+ Give options to cl(1):
+
+ $ li -I sbcl -e '(print 123)'
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+VERBOSE=false
+IMPL="${LISP_CLI_IMPL:-clisp}"
+while getopts ':I:vh' flag; do
+ case "$flag" in
+ I)
+ IMPL="$OPTARG"
+ ;;
+ v)
+ VERBOSE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+if [ "${1:-}" = '--' ]; then
+ shift
+fi
+
+
+IMAGE="${XDG_DATA_HOME:-$HOME/.local/share}/lisp-cli/$IMPL.image"
+BIN="$(command -v cl)"
+INIT="${XDG_CONFIG_HOME:-$HOME/.config}/lisp-cli/init.lisp"
+if [ ! -e "$IMAGE" ]; then
+ printf 'Bootstrapping a new "%s" image...\n' "$IMPL" >&2
+ cl \
+ -I "$IMPL" \
+ -v \
+ -e '(ql:quickload :trivial-dump-core)' \
+ -E "(trivial-dump-core:dump-image \"$IMAGE\")"
+elif [ -n "$(find "$0" "$BIN" "$INIT" -newer "$IMAGE")" ]; then
+ printf 'Refresh existing "%s" image...\n' "$IMPL" >&2
+ cl \
+ -M "$IMAGE" \
+ -I "$IMPL" \
+ -v \
+ -e '(ql:quickload :trivial-dump-core)' \
+ -E "(trivial-dump-core:dump-image \"$IMAGE\")"
+fi
+
+if [ "$VERBOSE" = true ]; then
+ set -x
+fi
+exec cl -M "$IMAGE" "$@"
diff --git a/bin/lines b/bin/lines
new file mode 100755
index 0000000..2f0bf46
--- /dev/null
+++ b/bin/lines
@@ -0,0 +1,81 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ lines START [END]
+ lines -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ START the nth line number to start showing
+ END the nth line number to stop
+ showing (default: START + 1)
+
+
+ Print the range START-END of lines of the content of STDIN.
+
+
+ Examples:
+
+ Print 3rd line:
+
+ $ seq 10 | lines 3
+ 3
+
+
+ Print lines 5~8:
+
+ $ lines 5 8 < file.txt
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+START="${1:-}"
+
+if [ -z "${2:-}" ]; then
+ END=1
+else
+ END=$(($2 - START + 1))
+fi
+
+eval "$(assert-arg "$START" 'START')"
+
+tail -n +"$START" | head -n "$END"
diff --git a/bin/m b/bin/m
new file mode 100755
index 0000000..6891128
--- /dev/null
+++ b/bin/m
@@ -0,0 +1,66 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ m
+ m -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Fetch email via IMAP and update the notmuch index.
+
+
+ Examples:
+
+ Just fetch email
+
+ $ m
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+F="${XDG_DATA_HOME:-$HOME/.local/share}"/euandreh/mailcfg-accounts.txt
+
+notmuch new
+xargs -I% -P "$(wc -l < "$F")" mbsync '%' < "$F"
+notmuch new
diff --git a/bin/mailcfg b/bin/mailcfg
new file mode 100755
index 0000000..f77355e
--- /dev/null
+++ b/bin/mailcfg
@@ -0,0 +1,297 @@
+#!/bin/sh
+# shellcheck disable=1090,2153
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ mailcfg ACTION
+ mailcfg -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - mbsync
+ - msmtp
+ - notmuchcfg
+ - notmuchhook
+ - alot
+ - list
+
+
+ Emit the generated configuration file for the chosen email
+ program. Get the configuration files from
+ $XDG_CONFIG_HOME/mailcfg/*.env, where every *.env file is a
+ shell script that defines the variables used in this program:
+ - $NAME
+ - $LABEL
+ - $IMAP
+ - $SMTP
+ - $ADDR
+
+ One of the files also needs to define:
+ - $DEFAULT_NAME
+ - $DEFAULT_ADDR
+ - $DEFAULT_LABEL
+
+ An example of such file could be "30-andre@work.com.env":
+
+ #!/bin/sh
+ set -eu
+
+
+ NAME='André!'
+ LABEL='Work'
+ IMAP='imap.work.com'
+ SMTP='smtp.work.com'
+ ADDR='andre@work.com'
+
+
+ Examples:
+
+ Get the alot configuration file:
+
+ $ mailcfg alot
+
+
+ List the existing account labels:
+
+ $ mailcfg list
+ EOF
+}
+
+
+for flag; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+ACTION="${1:-}"
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+CFGDIR="${XDG_CONFIG_HOME:-$HOME/.config}/mailcfg"
+
+
+mbsync() {
+ cat <<-'EOF'
+ SyncState *
+ Create Both
+ Expunge Both
+ Remove Both
+ Sync All
+ EOF
+
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ cat <<-EOF
+
+
+ ## $LABEL
+
+ IMAPAccount $LABEL
+ Host $IMAP
+ User $ADDR
+ PassCmd "pass show $ADDR"
+ SSLType IMAPS
+
+ IMAPStore ${LABEL}Remote
+ Account $LABEL
+
+ MaildirStore ${LABEL}Local
+ Path ~/Maildir/$LABEL/
+ Inbox ~/Maildir/$LABEL/INBOX
+ SubFolders Verbatim
+
+ Channel ${LABEL}Folders
+ Far :${LABEL}Remote:
+ Near :${LABEL}Local:
+ Patterns *
+
+ Group $LABEL
+ Channel ${LABEL}Folders
+ EOF
+ done
+}
+
+msmtp() {
+ cat <<-EOF
+ defaults
+ auth on
+ tls on
+ port 587
+ syslog on
+ logfile $XDG_LOG_HOME/msmtp.log
+ EOF
+
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ cat <<-EOF
+
+ account $LABEL
+ host $SMTP
+ from $ADDR
+ user $ADDR
+ passwordeval pass show $ADDR
+ EOF
+ done
+
+ cat <<-EOF
+
+ account default : $DEFAULT_LABEL
+ EOF
+}
+
+notmuchcfg() {
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ done
+
+ cat <<-EOF
+ [user]
+ name = $DEFAULT_NAME
+ primary_email = $DEFAULT_ADDR
+ EOF
+
+ printf 'other_email = '
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ if [ "$ADDR" = "$DEFAULT_ADDR" ]; then
+ continue
+ fi
+ echo "$ADDR"
+ done | paste -sd';'
+
+ cat <<-'EOF'
+
+ [new]
+ tags = new;
+ ignore = .mbsyncstate;.uidvalidity
+
+ [search]
+ exclude_tags = deleted;spam
+
+ [maildir]
+ synchronize_flags = true
+ EOF
+}
+
+notmuchhook() {
+ LABELS=''
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ if [ -z "$LABELS" ]; then
+ LABELS="$LABEL"
+ else
+ LABELS="$LABELS $LABEL"
+ fi
+ done
+ sed \
+ -e "s|@DIRS@|$LABELS|g" \
+ -e "s|@DEFAULT_LABEL@|$DEFAULT_LABEL|g" \
+ "$CFGDIR"/post-new
+}
+
+alot() {
+ cat <<-'EOF'
+ attachment_prefix = "~/Downloads/"
+
+ [bindings]
+ i = toggletags inbox
+ I = search folder:/INBOX/ AND NOT tag:killed AND NOT tag:archive
+ EOF
+ echo "
+ z archive
+ s spam
+ u unread
+ r keep
+ t track
+ " | while read -r l; do
+ if [ -z "$l" ]; then
+ continue
+ fi
+ LC="$( echo "$l" | cut -d' ' -f1)"
+ TAG="$(echo "$l" | cut -d' ' -f2)"
+ UC="$(echo "$LC" | tr '[:lower:]' '[:upper:]')"
+ cat <<-EOF
+ $LC = toggletags $TAG
+ $UC = search tag:$TAG AND NOT tag:killed
+ EOF
+ done
+
+ cat <<-'EOF'
+ M = search folder:/lists/ AND NOT tag:killed
+ m = compose --tags inbox
+ [[thread]]
+ v = pipeto urlscan 2>/dev/null
+ V = pipeto 'gpg -d | less'
+ r = reply --all
+ R = reply
+ ' ' = fold; untag unread; move next unfolded
+ P = pipeto 'git am'
+
+ [accounts]
+ EOF
+
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ cat <<-EOF
+ [[$LABEL]]
+ realname = $NAME
+ address = $ADDR
+ sendmail_command = msmtpq --account=$LABEL -t
+ sent_box = maildir://~/Maildir/$LABEL/Sent
+ draft_box = maildir://~/Maildir/$LABEL/Drafts
+ gpg_key = 5BDAE9B8B2F6C6BCBB0D6CE581F90EC3CD356060
+ EOF
+ done
+}
+
+list() {
+ for env in "$CFGDIR"/*.env; do
+ . "$env"
+ printf '%s\n' "$LABEL"
+ done
+}
+
+
+case "$ACTION" in
+ mbsync|msmtp|notmuchcfg|notmuchhook|alot|list)
+ "$1"
+ ;;
+ *)
+ printf 'Unsupported ACTION: "%s".\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/max b/bin/max
new file mode 100755
index 0000000..ae38983
--- /dev/null
+++ b/bin/max
@@ -0,0 +1,84 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ max NUMBER...
+ max -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ Get the maximum number from the given values.
+
+
+ Examples:
+
+ Get the maximum number from a list:
+
+ $ min 5 3 9 9 4
+ 9
+
+ Get the maximum number when negative numbers are given
+
+ $ max -- -3 -5
+ -3
+
+ Get the maximum number given a single number
+
+ $ max 8
+ 8
+
+ The maximum default number:
+
+ $ max
+ 0
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${1:-}" ]; then
+ echo 0
+ exit
+fi
+
+N="$1"
+for n in "$@"; do
+ N=$((N > n ? N : n))
+done
+echo "$N"
diff --git a/bin/menu b/bin/menu
new file mode 100755
index 0000000..90af758
--- /dev/null
+++ b/bin/menu
@@ -0,0 +1,1587 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ menu ACTION
+ menu -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - emoji
+ - username
+ - password
+ - bin
+ - clipboard
+ - yubikey
+
+
+ Lauch an interactive GUI menu for various desktop activities, to
+ be launched manually or via desktop keybindings.
+
+
+ Examples:
+
+ Choose and copy the password to the clipboard:
+
+ $ menu password
+
+
+ Execute a binary available in $PATH:
+
+ $ menu bin
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+ACTION="${1:-}"
+
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+
+DATA() {
+ awk 'd == 1 { print; next } /^__DATA__$/ { d = 1 }' "$0" |
+ head -n -1 # trim ShellCheck quote
+}
+
+show() {
+ dmenu -i -l 20 -fn Monospace-18 -p "$1:"
+}
+
+pass_list() {
+ walk "$PASSWORD_STORE_DIR" |
+ grep '\.gpg$' |
+ sor 'test -f' |
+ sed -e "s|^$PASSWORD_STORE_DIR/||" \
+ -e 's|\.gpg$||' |
+ LANG=POSIX.UTF-8 sort
+}
+
+case "$ACTION" in
+ emoji)
+ DATA | show 'emoji' | awk '{print $(NF)}' | copy -n
+ ;;
+ username)
+ CHOICE="$(pass_list | show 'username')"
+ if [ -n "$CHOICE" ]; then
+ if pass show -c2 "$CHOICE"; then
+ notify-send -t 5000 -u normal -- \
+ "$CHOICE" 'username copied to clipboard'
+ fi
+ fi
+ ;;
+ password)
+ CHOICE="$(pass_list | show 'password')"
+ if [ -n "$CHOICE" ]; then
+ if pass show -c1 "$CHOICE"; then
+ notify-send -t 5000 -u critical -- \
+ "$CHOICE" 'password copied to clipboard'
+ fi
+ fi
+ ;;
+ bin)
+ CHOICE="$(bins | show 'bins')"
+ if [ -n "$CHOICE" ]; then
+ exec "$CHOICE"
+ fi
+ ;;
+ clipboard)
+ # For a potential improved version, see:
+ # https://github.com/cdown/clipmenu/pull/162
+ clipmenu -i -l 20 -fn Monospace-18 -p "$1:"
+ notify-send -t 5000 -u low -- 'copied to clipboard'
+ ;;
+ yubikey)
+ CHOICE="$(ykman oath accounts list | show 'OTP')"
+ if [ -n "$CHOICE" ]; then
+ ykman oath accounts code "$CHOICE" |
+ awk '{ print $(NF) }' |
+ copy -n
+ notify-send -t 5000 -u normal -- \
+ "$CHOICE" 'code copied to clipboard'
+ fi
+ ;;
+ *)
+ printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
+
+
+
+exit
+
+# Make ShellCheck happy. See https://github.com/koalaman/shellcheck/issues/1201
+# shellcheck disable=1112
+echo '
+__DATA__
+grinning face 😀
+smiling face with open mouth 😃
+smiling face with open mouth & smiling eyes 😄
+grinning face with smiling eyes 😁
+smiling face with open mouth & closed eyes 😆
+smiling face with open mouth & cold sweat 😅
+face with tears of joy 😂
+rolling on the floor laughing 🤣
+smiling face ☺️
+smiling face with smiling eyes 😊
+smiling face with halo 😇
+slightly smiling face 🙂
+upside-down face 🙃
+winking face 😉
+relieved face 😌
+smiling face with heart-eyes 😍
+face blowing a kiss 😘
+kissing face 😗
+kissing face with smiling eyes 😙
+kissing face with closed eyes 😚
+face savouring delicious food 😋
+face with stuck-out tongue & winking eye 😜
+face with stuck-out tongue & closed eyes 😝
+face with stuck-out tongue 😛
+money-mouth face 🤑
+hugging face 🤗
+nerd face 🤓
+smiling face with sunglasses 😎
+clown face 🤡
+cowboy hat face 🤠
+smirking face 😏
+unamused face 😒
+disappointed face 😞
+pensive face 😔
+worried face 😟
+confused face 😕
+slightly frowning face 🙁
+frowning face ☹️
+persevering face 😣
+confounded face 😖
+tired face 😫
+weary face 😩
+face with steam from nose 😤
+angry face 😠
+pouting face 😡
+face without mouth 😶
+neutral face 😐
+expressionless face 😑
+hushed face 😯
+frowning face with open mouth 😦
+anguished face 😧
+face with open mouth 😮
+astonished face 😲
+dizzy face 😵
+flushed face 😳
+face screaming in fear 😱
+fearful face 😨
+face with open mouth & cold sweat 😰
+crying face 😢
+disappointed but relieved face 😥
+drooling face 🤤
+loudly crying face 😭
+face with cold sweat 😓
+sleepy face 😪
+sleeping face 😴
+face with rolling eyes 🙄
+thinking face 🤔
+lying face 🤥
+grimacing face 😬
+zipper-mouth face 🤐
+nauseated face 🤢
+sneezing face 🤧
+face with medical mask 😷
+face with thermometer 🤒
+face with head-bandage 🤕
+smiling face with horns 😈
+angry face with horns 👿
+ogre 👹
+goblin 👺
+pile of poo 💩
+ghost 👻
+skull 💀
+skull and crossbones ☠️
+alien 👽
+alien monster 👾
+robot face 🤖
+jack-o-lantern 🎃
+smiling cat face with open mouth 😺
+grinning cat face with smiling eyes 😸
+cat face with tears of joy 😹
+smiling cat face with heart-eyes 😻
+cat face with wry smile 😼
+kissing cat face with closed eyes 😽
+weary cat face 🙀
+crying cat face 😿
+pouting cat face 😾
+open hands 👐
+raising hands 🙌
+clapping hands 👏
+folded hands 🙏
+handshake 🤝
+thumbs up 👍
+thumbs down 👎
+oncoming fist 👊
+raised fist ✊
+left-facing fist 🤛
+right-facing fist 🤜
+crossed fingers 🤞
+victory hand ✌️
+sign of the horns 🤘
+OK hand 👌
+backhand index pointing left 👈
+backhand index pointing right 👉
+backhand index pointing up 👆
+backhand index pointing down 👇
+index pointing up ☝️
+raised hand ✋
+raised back of hand 🤚
+raised hand with fingers splayed 🖐
+vulcan salute 🖖
+waving hand 👋
+call me hand 🤙
+flexed biceps 💪
+middle finger 🖕
+writing hand ✍️
+selfie 🤳
+nail polish 💅
+ring 💍
+lipstick 💄
+kiss mark 💋
+mouth 👄
+tongue 👅
+ear 👂
+nose 👃
+footprints 👣
+eye 👁
+eyes 👀
+speaking head 🗣
+bust in silhouette 👤
+busts in silhouette 👥
+baby 👶
+boy 👦
+girl 👧
+man 👨
+woman 👩
+blond-haired woman 👱‍♀
+blond-haired person 👱
+old man 👴
+old woman 👵
+man with Chinese cap 👲
+woman wearing turban 👳‍♀
+person wearing turban 👳
+woman police officer 👮‍♀
+police officer 👮
+woman construction worker 👷‍♀
+construction worker 👷
+woman guard 💂‍♀
+guard 💂
+woman detective 🕵️‍♀️
+detective 🕵
+woman health worker 👩‍⚕
+man health worker 👨‍⚕
+woman farmer 👩‍🌾
+man farmer 👨‍🌾
+woman cook 👩‍🍳
+man cook 👨‍🍳
+woman student 👩‍🎓
+man student 👨‍🎓
+woman singer 👩‍🎤
+man singer 👨‍🎤
+woman teacher 👩‍🏫
+man teacher 👨‍🏫
+woman factory worker 👩‍🏭
+man factory worker 👨‍🏭
+woman technologist 👩‍💻
+man technologist 👨‍💻
+woman office worker 👩‍💼
+man office worker 👨‍💼
+woman mechanic 👩‍🔧
+man mechanic 👨‍🔧
+woman scientist 👩‍🔬
+man scientist 👨‍🔬
+woman artist 👩‍🎨
+man artist 👨‍🎨
+woman firefighter 👩‍🚒
+man firefighter 👨‍🚒
+woman pilot 👩‍✈
+man pilot 👨‍✈
+woman astronaut 👩‍🚀
+man astronaut 👨‍🚀
+woman judge 👩‍⚖
+man judge 👨‍⚖
+Mrs. Claus 🤶
+Santa Claus 🎅
+princess 👸
+prince 🤴
+bride with veil 👰
+man in tuxedo 🤵
+baby angel 👼
+pregnant woman 🤰
+woman bowing 🙇‍♀
+person bowing 🙇
+person tipping hand 💁
+man tipping hand 💁‍♂
+person gesturing NO 🙅
+man gesturing NO 🙅‍♂
+person gesturing OK 🙆
+man gesturing OK 🙆‍♂
+person raising hand 🙋
+man raising hand 🙋‍♂
+woman facepalming 🤦‍♀
+man facepalming 🤦‍♂
+woman shrugging 🤷‍♀
+man shrugging 🤷‍♂
+person pouting 🙎
+man pouting 🙎‍♂
+person frowning 🙍
+man frowning 🙍‍♂
+person getting haircut 💇
+man getting haircut 💇‍♂
+person getting massage 💆
+man getting massage 💆‍♂
+man in business suit levitating 🕴
+woman dancing 💃
+man dancing 🕺
+people with bunny ears partying 👯
+men with bunny ears partying 👯‍♂
+woman walking 🚶‍♀
+person walking 🚶
+woman running 🏃‍♀
+person running 🏃
+man and woman holding hands 👫
+two women holding hands 👭
+two men holding hands 👬
+couple with heart 💑
+couple with heart: woman woman 👩‍❤️‍👩
+couple with heart: man man 👨‍❤️‍👨
+kiss 💏
+kiss: woman woman 👩‍❤️‍💋‍👩
+kiss: man man 👨‍❤️‍💋‍👨
+family 👪
+family: man woman girl 👨‍👩‍👧
+family: man woman girl boy 👨‍👩‍👧‍👦
+family: man woman boy boy 👨‍👩‍👦‍👦
+family: man woman girl girl 👨‍👩‍👧‍👧
+family: woman woman boy 👩‍👩‍👦
+family: woman woman girl 👩‍👩‍👧
+family: woman woman girl boy 👩‍👩‍👧‍👦
+family: woman woman boy boy 👩‍👩‍👦‍👦
+family: woman woman girl girl 👩‍👩‍👧‍👧
+family: man man boy 👨‍👨‍👦
+family: man man girl 👨‍👨‍👧
+family: man man girl boy 👨‍👨‍👧‍👦
+family: man man boy boy 👨‍👨‍👦‍👦
+family: man man girl girl 👨‍👨‍👧‍👧
+family: woman boy 👩‍👦
+family: woman girl 👩‍👧
+family: woman girl boy 👩‍👧‍👦
+family: woman boy boy 👩‍👦‍👦
+family: woman girl girl 👩‍👧‍👧
+family: man boy 👨‍👦
+family: man girl 👨‍👧
+family: man girl boy 👨‍👧‍👦
+family: man boy boy 👨‍👦‍👦
+family: man girl girl 👨‍👧‍👧
+woman’s clothes 👚
+t-shirt 👕
+jeans 👖
+necktie 👔
+dress 👗
+bikini 👙
+kimono 👘
+high-heeled shoe 👠
+woman’s sandal 👡
+woman’s boot 👢
+man’s shoe 👞
+running shoe 👟
+woman’s hat 👒
+top hat 🎩
+graduation cap 🎓
+crown 👑
+rescue worker’s helmet ⛑
+school backpack 🎒
+clutch bag 👝
+purse 👛
+handbag 👜
+briefcase 💼
+glasses 👓
+sunglasses 🕶
+closed umbrella 🌂
+umbrella ☂️
+dog face 🐶
+cat face 🐱
+mouse face 🐭
+hamster face 🐹
+rabbit face 🐰
+fox face 🦊
+bear face 🐻
+panda face 🐼
+koala 🐨
+tiger face 🐯
+lion face 🦁
+cow face 🐮
+pig face 🐷
+pig nose 🐽
+frog face 🐸
+monkey face 🐵
+see-no-evil monkey 🙈
+hear-no-evil monkey 🙉
+speak-no-evil monkey 🙊
+monkey 🐒
+chicken 🐔
+penguin 🐧
+bird 🐦
+baby chick 🐤
+hatching chick 🐣
+front-facing baby chick 🐥
+duck 🦆
+eagle 🦅
+owl 🦉
+bat 🦇
+wolf face 🐺
+boar 🐗
+horse face 🐴
+unicorn face 🦄
+honeybee 🐝
+bug 🐛
+butterfly 🦋
+snail 🐌
+spiral shell 🐚
+lady beetle 🐞
+ant 🐜
+spider 🕷
+spider web 🕸
+turtle 🐢
+snake 🐍
+lizard 🦎
+scorpion 🦂
+crab 🦀
+squid 🦑
+octopus 🐙
+shrimp 🦐
+tropical fish 🐠
+fish 🐟
+blowfish 🐡
+dolphin 🐬
+shark 🦈
+spouting whale 🐳
+whale 🐋
+crocodile 🐊
+leopard 🐆
+tiger 🐅
+water buffalo 🐃
+ox 🐂
+cow 🐄
+deer 🦌
+camel 🐪
+two-hump camel 🐫
+elephant 🐘
+rhinoceros 🦏
+gorilla 🦍
+horse 🐎
+pig 🐖
+goat 🐐
+ram 🐏
+sheep 🐑
+dog 🐕
+poodle 🐩
+cat 🐈
+rooster 🐓
+turkey 🦃
+dove 🕊
+rabbit 🐇
+mouse 🐁
+rat 🐀
+chipmunk 🐿
+paw prints 🐾
+dragon 🐉
+dragon face 🐲
+cactus 🌵
+Christmas tree 🎄
+evergreen tree 🌲
+deciduous tree 🌳
+palm tree 🌴
+seedling 🌱
+herb 🌿
+shamrock ☘️
+four leaf clover 🍀
+pine decoration 🎍
+tanabata tree 🎋
+leaf fluttering in wind 🍃
+fallen leaf 🍂
+maple leaf 🍁
+mushroom 🍄
+sheaf of rice 🌾
+bouquet 💐
+tulip 🌷
+rose 🌹
+wilted flower 🥀
+sunflower 🌻
+blossom 🌼
+cherry blossom 🌸
+hibiscus 🌺
+globe showing Americas 🌎
+globe showing Europe-Africa 🌍
+globe showing Asia-Australia 🌏
+full moon 🌕
+waning gibbous moon 🌖
+last quarter moon 🌗
+waning crescent moon 🌘
+new moon 🌑
+waxing crescent moon 🌒
+first quarter moon 🌓
+waxing gibbous moon 🌔
+new moon face 🌚
+full moon with face 🌝
+sun with face 🌞
+first quarter moon with face 🌛
+last quarter moon with face 🌜
+crescent moon 🌙
+dizzy 💫
+white medium star ⭐️
+glowing star 🌟
+sparkles ✨
+high voltage ⚡️
+fire 🔥
+collision 💥
+comet ☄
+sun ☀️
+sun behind small cloud 🌤
+sun behind cloud ⛅️
+sun behind large cloud 🌥
+sun behind rain cloud 🌦
+rainbow 🌈
+cloud ☁️
+cloud with rain 🌧
+cloud with lightning and rain ⛈
+cloud with lightning 🌩
+cloud with snow 🌨
+snowman ☃️
+snowman without snow ⛄️
+snowflake ❄️
+wind face 🌬
+dashing away 💨
+tornado 🌪
+fog 🌫
+water wave 🌊
+droplet 💧
+sweat droplets 💦
+umbrella with rain drops ☔️
+green apple 🍏
+red apple 🍎
+pear 🍐
+tangerine 🍊
+lemon 🍋
+banana 🍌
+watermelon 🍉
+grapes 🍇
+strawberry 🍓
+melon 🍈
+cherries 🍒
+peach 🍑
+pineapple 🍍
+kiwi fruit 🥝
+avocado 🥑
+tomato 🍅
+eggplant 🍆
+cucumber 🥒
+carrot 🥕
+ear of corn 🌽
+hot pepper 🌶
+potato 🥔
+roasted sweet potato 🍠
+chestnut 🌰
+peanuts 🥜
+honey pot 🍯
+croissant 🥐
+bread 🍞
+baguette bread 🥖
+cheese wedge 🧀
+egg 🥚
+cooking 🍳
+bacon 🥓
+pancakes 🥞
+fried shrimp 🍤
+poultry leg 🍗
+meat on bone 🍖
+pizza 🍕
+hot dog 🌭
+hamburger 🍔
+french fries 🍟
+stuffed flatbread 🥙
+taco 🌮
+burrito 🌯
+green salad 🥗
+shallow pan of food 🥘
+spaghetti 🍝
+steaming bowl 🍜
+pot of food 🍲
+fish cake with swirl 🍥
+sushi 🍣
+bento box 🍱
+curry rice 🍛
+cooked rice 🍚
+rice ball 🍙
+rice cracker 🍘
+oden 🍢
+dango 🍡
+shaved ice 🍧
+ice cream 🍨
+soft ice cream 🍦
+shortcake 🍰
+birthday cake 🎂
+custard 🍮
+lollipop 🍭
+candy 🍬
+chocolate bar 🍫
+popcorn 🍿
+doughnut 🍩
+cookie 🍪
+glass of milk 🥛
+baby bottle 🍼
+hot beverage ☕️
+teacup without handle 🍵
+sake 🍶
+beer mug 🍺
+clinking beer mugs 🍻
+clinking glasses 🥂
+wine glass 🍷
+tumbler glass 🥃
+cocktail glass 🍸
+tropical drink 🍹
+bottle with popping cork 🍾
+spoon 🥄
+fork and knife 🍴
+fork and knife with plate 🍽
+soccer ball ⚽️
+basketball 🏀
+american football 🏈
+baseball ⚾️
+tennis 🎾
+volleyball 🏐
+rugby football 🏉
+pool 8 ball 🎱
+ping pong 🏓
+badminton 🏸
+goal net 🥅
+ice hockey 🏒
+field hockey 🏑
+cricket 🏏
+flag in hole ⛳️
+bow and arrow 🏹
+fishing pole 🎣
+boxing glove 🥊
+martial arts uniform 🥋
+ice skate ⛸
+skis 🎿
+skier ⛷
+snowboarder 🏂
+woman lifting weights 🏋️‍♀️
+person lifting weights 🏋
+person fencing 🤺
+women wrestling 🤼‍♀
+men wrestling 🤼‍♂
+woman cartwheeling 🤸‍♀
+man cartwheeling 🤸‍♂
+woman bouncing ball ⛹️‍♀️
+person bouncing ball ⛹
+woman playing handball 🤾‍♀
+man playing handball 🤾‍♂
+woman golfing 🏌️‍♀️
+person golfing 🏌
+woman surfing 🏄‍♀
+person surfing 🏄
+woman swimming 🏊‍♀
+person swimming 🏊
+woman playing water polo 🤽‍♀
+man playing water polo 🤽‍♂
+woman rowing boat 🚣‍♀
+person rowing boat 🚣
+horse racing 🏇
+woman biking 🚴‍♀
+person biking 🚴
+woman mountain biking 🚵‍♀
+person mountain biking 🚵
+running shirt 🎽
+sports medal 🏅
+military medal 🎖
+1st place medal 🥇
+2nd place medal 🥈
+3rd place medal 🥉
+trophy 🏆
+rosette 🏵
+reminder ribbon 🎗
+ticket 🎫
+admission tickets 🎟
+circus tent 🎪
+woman juggling 🤹‍♀
+man juggling 🤹‍♂
+performing arts 🎭
+artist palette 🎨
+clapper board 🎬
+microphone 🎤
+headphone 🎧
+musical score 🎼
+musical keyboard 🎹
+drum 🥁
+saxophone 🎷
+trumpet 🎺
+guitar 🎸
+violin 🎻
+game die 🎲
+direct hit 🎯
+bowling 🎳
+video game 🎮
+slot machine 🎰
+automobile 🚗
+taxi 🚕
+sport utility vehicle 🚙
+bus 🚌
+trolleybus 🚎
+racing car 🏎
+police car 🚓
+ambulance 🚑
+fire engine 🚒
+minibus 🚐
+delivery truck 🚚
+articulated lorry 🚛
+tractor 🚜
+kick scooter 🛴
+bicycle 🚲
+motor scooter 🛵
+motorcycle 🏍
+police car light 🚨
+oncoming police car 🚔
+oncoming bus 🚍
+oncoming automobile 🚘
+oncoming taxi 🚖
+aerial tramway 🚡
+mountain cableway 🚠
+suspension railway 🚟
+railway car 🚃
+tram car 🚋
+mountain railway 🚞
+monorail 🚝
+high-speed train 🚄
+high-speed train with bullet nose 🚅
+light rail 🚈
+locomotive 🚂
+train 🚆
+metro 🚇
+tram 🚊
+station 🚉
+helicopter 🚁
+small airplane 🛩
+airplane ✈️
+airplane departure 🛫
+airplane arrival 🛬
+rocket 🚀
+satellite 🛰
+seat 💺
+canoe 🛶
+sailboat ⛵️
+motor boat 🛥
+speedboat 🚤
+passenger ship 🛳
+ferry ⛴
+ship 🚢
+anchor ⚓️
+construction 🚧
+fuel pump ⛽️
+bus stop 🚏
+vertical traffic light 🚦
+horizontal traffic light 🚥
+world map 🗺
+moai 🗿
+Statue of Liberty 🗽
+fountain ⛲️
+Tokyo tower 🗼
+castle 🏰
+Japanese castle 🏯
+stadium 🏟
+ferris wheel 🎡
+roller coaster 🎢
+carousel horse 🎠
+umbrella on ground ⛱
+beach with umbrella 🏖
+desert island 🏝
+mountain ⛰
+snow-capped mountain 🏔
+mount fuji 🗻
+volcano 🌋
+desert 🏜
+camping 🏕
+tent ⛺️
+railway track 🛤
+motorway 🛣
+building construction 🏗
+factory 🏭
+house 🏠
+house with garden 🏡
+house 🏘
+derelict house 🏚
+office building 🏢
+department store 🏬
+Japanese post office 🏣
+post office 🏤
+hospital 🏥
+bank 🏦
+hotel 🏨
+convenience store 🏪
+school 🏫
+love hotel 🏩
+wedding 💒
+classical building 🏛
+church ⛪️
+mosque 🕌
+synagogue 🕍
+kaaba 🕋
+shinto shrine ⛩
+map of Japan 🗾
+moon viewing ceremony 🎑
+national park 🏞
+sunrise 🌅
+sunrise over mountains 🌄
+shooting star 🌠
+sparkler 🎇
+fireworks 🎆
+sunset 🌇
+cityscape at dusk 🌆
+cityscape 🏙
+night with stars 🌃
+milky way 🌌
+bridge at night 🌉
+foggy 🌁
+watch ⌚️
+mobile phone 📱
+mobile phone with arrow 📲
+laptop computer 💻
+keyboard ⌨️
+desktop computer 🖥
+printer 🖨
+computer mouse 🖱
+trackball 🖲
+joystick 🕹
+clamp 🗜
+computer disk 💽
+floppy disk 💾
+optical disk 💿
+dvd 📀
+videocassette 📼
+camera 📷
+camera with flash 📸
+video camera 📹
+movie camera 🎥
+film projector 📽
+film frames 🎞
+telephone receiver 📞
+telephone ☎️
+pager 📟
+fax machine 📠
+television 📺
+radio 📻
+studio microphone 🎙
+level slider 🎚
+control knobs 🎛
+stopwatch ⏱
+timer clock ⏲
+alarm clock ⏰
+mantelpiece clock 🕰
+hourglass ⌛️
+hourglass with flowing sand ⏳
+satellite antenna 📡
+battery 🔋
+electric plug 🔌
+light bulb 💡
+flashlight 🔦
+candle 🕯
+wastebasket 🗑
+oil drum 🛢
+money with wings 💸
+dollar banknote 💵
+yen banknote 💴
+euro banknote 💶
+pound banknote 💷
+money bag 💰
+credit card 💳
+gem stone 💎
+balance scale ⚖️
+wrench 🔧
+hammer 🔨
+hammer and pick ⚒
+hammer and wrench 🛠
+pick ⛏
+nut and bolt 🔩
+gear ⚙️
+chains ⛓
+pistol 🔫
+bomb 💣
+kitchen knife 🔪
+dagger 🗡
+crossed swords ⚔️
+shield 🛡
+cigarette 🚬
+coffin ⚰️
+funeral urn ⚱️
+amphora 🏺
+crystal ball 🔮
+prayer beads 📿
+barber pole 💈
+alembic ⚗️
+telescope 🔭
+microscope 🔬
+hole 🕳
+pill 💊
+syringe 💉
+thermometer 🌡
+toilet 🚽
+potable water 🚰
+shower 🚿
+bathtub 🛁
+person taking bath 🛀
+bellhop bell 🛎
+key 🔑
+old key 🗝
+door 🚪
+couch and lamp 🛋
+bed 🛏
+person in bed 🛌
+framed picture 🖼
+shopping bags 🛍
+shopping cart 🛒
+wrapped gift 🎁
+balloon 🎈
+carp streamer 🎏
+ribbon 🎀
+confetti ball 🎊
+party popper 🎉
+Japanese dolls 🎎
+red paper lantern 🏮
+wind chime 🎐
+envelope ✉️
+envelope with arrow 📩
+incoming envelope 📨
+e-mail 📧
+love letter 💌
+inbox tray 📥
+outbox tray 📤
+package 📦
+label 🏷
+closed mailbox with lowered flag 📪
+closed mailbox with raised flag 📫
+open mailbox with raised flag 📬
+open mailbox with lowered flag 📭
+postbox 📮
+postal horn 📯
+scroll 📜
+page with curl 📃
+page facing up 📄
+bookmark tabs 📑
+bar chart 📊
+chart increasing 📈
+chart decreasing 📉
+spiral notepad 🗒
+spiral calendar 🗓
+tear-off calendar 📆
+calendar 📅
+card index 📇
+card file box 🗃
+ballot box with ballot 🗳
+file cabinet 🗄
+clipboard 📋
+file folder 📁
+open file folder 📂
+card index dividers 🗂
+rolled-up newspaper 🗞
+newspaper 📰
+notebook 📓
+notebook with decorative cover 📔
+ledger 📒
+closed book 📕
+green book 📗
+blue book 📘
+orange book 📙
+books 📚
+open book 📖
+bookmark 🔖
+link 🔗
+paperclip 📎
+linked paperclips 🖇
+triangular ruler 📐
+straight ruler 📏
+pushpin 📌
+round pushpin 📍
+scissors ✂️
+pen 🖊
+fountain pen 🖋
+black nib ✒️
+paintbrush 🖌
+crayon 🖍
+memo 📝
+pencil ✏️
+left-pointing magnifying glass 🔍
+right-pointing magnifying glass 🔎
+locked with pen 🔏
+locked with key 🔐
+locked 🔒
+unlocked 🔓
+red heart ❤️
+yellow heart 💛
+green heart 💚
+blue heart 💙
+purple heart 💜
+black heart 🖤
+broken heart 💔
+heavy heart exclamation ❣️
+two hearts 💕
+revolving hearts 💞
+beating heart 💓
+growing heart 💗
+sparkling heart 💖
+heart with arrow 💘
+heart with ribbon 💝
+heart decoration 💟
+peace symbol ☮️
+latin cross ✝️
+star and crescent ☪️
+om 🕉
+wheel of dharma ☸️
+star of David ✡️
+dotted six-pointed star 🔯
+menorah 🕎
+yin yang ☯️
+orthodox cross ☦️
+place of worship 🛐
+Ophiuchus ⛎
+Aries ♈️
+Taurus ♉️
+Gemini ♊️
+Cancer ♋️
+Leo ♌️
+Virgo ♍️
+Libra ♎️
+Scorpius ♏️
+Sagittarius ♐️
+Capricorn ♑️
+Aquarius ♒️
+Pisces ♓️
+ID button 🆔
+atom symbol ⚛️
+Japanese “acceptable” button 🉑
+radioactive ☢️
+biohazard ☣️
+mobile phone off 📴
+vibration mode 📳
+Japanese “not free of charge” button 🈶
+Japanese “free of charge” button 🈚️
+Japanese “application” button 🈸
+Japanese “open for business” button 🈺
+Japanese “monthly amount” button 🈷️
+eight-pointed star ✴️
+VS button 🆚
+white flower 💮
+Japanese “bargain” button 🉐
+Japanese “secret” button ㊙️
+Japanese “congratulations” button ㊗️
+Japanese “passing grade” button 🈴
+Japanese “no vacancy” button 🈵
+Japanese “discount” button 🈹
+Japanese “prohibited” button 🈲
+A button (blood type) 🅰️
+B button (blood type) 🅱️
+AB button (blood type) 🆎
+CL button 🆑
+O button (blood type) 🅾️
+SOS button 🆘
+cross mark ❌
+heavy large circle ⭕️
+stop sign 🛑
+no entry ⛔️
+name badge 📛
+prohibited 🚫
+hundred points 💯
+anger symbol 💢
+hot springs ♨️
+no pedestrians 🚷
+no littering 🚯
+no bicycles 🚳
+non-potable water 🚱
+no one under eighteen 🔞
+no mobile phones 📵
+no smoking 🚭
+exclamation mark ❗️
+white exclamation mark ❕
+question mark ❓
+white question mark ❔
+double exclamation mark ‼️
+exclamation question mark ⁉️
+dim button 🔅
+bright button 🔆
+part alternation mark 〽️
+warning ⚠️
+children crossing 🚸
+trident emblem 🔱
+fleur-de-lis ⚜️
+Japanese symbol for beginner 🔰
+recycling symbol ♻️
+white heavy check mark ✅
+Japanese “reserved” button 🈯️
+chart increasing with yen 💹
+sparkle ❇️
+eight-spoked asterisk ✳️
+cross mark button ❎
+globe with meridians 🌐
+diamond with a dot 💠
+circled M Ⓜ️
+cyclone 🌀
+zzz 💤
+ATM sign 🏧
+water closet 🚾
+wheelchair symbol ♿️
+P button 🅿️
+Japanese “vacancy” button 🈳
+Japanese “service charge” button 🈂️
+passport control 🛂
+customs 🛃
+baggage claim 🛄
+left luggage 🛅
+men’s room 🚹
+women’s room 🚺
+baby symbol 🚼
+restroom 🚻
+litter in bin sign 🚮
+cinema 🎦
+antenna bars 📶
+Japanese “here” button 🈁
+input symbols 🔣
+information ℹ️
+input latin letters 🔤
+input latin lowercase 🔡
+input latin uppercase 🔠
+NG button 🆖
+OK button 🆗
+UP! button 🆙
+COOL button 🆒
+NEW button 🆕
+FREE button 🆓
+keycap: 0 0️⃣
+keycap: 1 1️⃣
+keycap: 2 2️⃣
+keycap: 3 3️⃣
+keycap: 4 4️⃣
+keycap: 5 5️⃣
+keycap: 6 6️⃣
+keycap: 7 7️⃣
+keycap: 8 8️⃣
+keycap: 9 9️⃣
+keycap 10 🔟
+input numbers 🔢
+keycap: # #️⃣
+keycap: * *️⃣
+play button ▶️
+pause button ⏸
+play or pause button ⏯
+stop button ⏹
+record button ⏺
+next track button ⏭
+last track button ⏮
+fast-forward button ⏩
+fast reverse button ⏪
+fast up button ⏫
+fast down button ⏬
+reverse button ◀️
+up button 🔼
+down button 🔽
+right arrow ➡️
+left arrow ⬅️
+up arrow ⬆️
+down arrow ⬇️
+up-right arrow ↗️
+down-right arrow ↘️
+down-left arrow ↙️
+up-left arrow ↖️
+up-down arrow ↕️
+left-right arrow ↔️
+left arrow curving right ↪️
+right arrow curving left ↩️
+right arrow curving up ⤴️
+right arrow curving down ⤵️
+shuffle tracks button 🔀
+repeat button 🔁
+repeat single button 🔂
+anticlockwise arrows button 🔄
+clockwise vertical arrows 🔃
+musical note 🎵
+musical notes 🎶
+heavy plus sign ➕
+heavy minus sign ➖
+heavy division sign ➗
+heavy multiplication x ✖️
+heavy dollar sign 💲
+currency exchange 💱
+trade mark ™️
+copyright ©️
+registered ®️
+wavy dash 〰️
+curly loop ➰
+double curly loop ➿
+END arrow 🔚
+BACK arrow 🔙
+ON! arrow 🔛
+TOP arrow 🔝
+SOON arrow 🔜
+heavy check mark ✔️
+ballot box with check ☑️
+radio button 🔘
+white circle ⚪️
+black circle ⚫️
+red circle 🔴
+blue circle 🔵
+red triangle pointed up 🔺
+red triangle pointed down 🔻
+small orange diamond 🔸
+small blue diamond 🔹
+large orange diamond 🔶
+large blue diamond 🔷
+white square button 🔳
+black square button 🔲
+black small square ▪️
+white small square ▫️
+black medium-small square ◾️
+white medium-small square ◽️
+black medium square ◼️
+white medium square ◻️
+black large square ⬛️
+white large square ⬜️
+speaker low volume 🔈
+muted speaker 🔇
+speaker medium volume 🔉
+speaker high volume 🔊
+bell 🔔
+bell with slash 🔕
+megaphone 📣
+loudspeaker 📢
+eye in speech bubble 👁‍🗨
+speech balloon 💬
+thought balloon 💭
+right anger bubble 🗯
+spade suit ♠️
+club suit ♣️
+heart suit ♥️
+diamond suit ♦️
+joker 🃏
+flower playing cards 🎴
+mahjong red dragon 🀄️
+one o’clock 🕐
+two o’clock 🕑
+three o’clock 🕒
+four o’clock 🕓
+five o’clock 🕔
+six o’clock 🕕
+seven o’clock 🕖
+eight o’clock 🕗
+nine o’clock 🕘
+ten o’clock 🕙
+eleven o’clock 🕚
+twelve o’clock 🕛
+one-thirty 🕜
+two-thirty 🕝
+three-thirty 🕞
+four-thirty 🕟
+five-thirty 🕠
+six-thirty 🕡
+seven-thirty 🕢
+eight-thirty 🕣
+nine-thirty 🕤
+ten-thirty 🕥
+eleven-thirty 🕦
+twelve-thirty 🕧
+white flag 🏳️
+black flag 🏴
+chequered flag 🏁
+triangular flag 🚩
+rainbow flag 🏳️‍🌈
+Afghanistan 🇦🇫
+Åland Islands 🇦🇽
+Albania 🇦🇱
+Algeria 🇩🇿
+American Samoa 🇦🇸
+Andorra 🇦🇩
+Angola 🇦🇴
+Anguilla 🇦🇮
+Antarctica 🇦🇶
+Antigua & Barbuda 🇦🇬
+Argentina 🇦🇷
+Armenia 🇦🇲
+Aruba 🇦🇼
+Australia 🇦🇺
+Austria 🇦🇹
+Azerbaijan 🇦🇿
+Bahamas 🇧🇸
+Bahrain 🇧🇭
+Bangladesh 🇧🇩
+Barbados 🇧🇧
+Belarus 🇧🇾
+Belgium 🇧🇪
+Belize 🇧🇿
+Benin 🇧🇯
+Bermuda 🇧🇲
+Bhutan 🇧🇹
+Bolivia 🇧🇴
+Caribbean Netherlands 🇧🇶
+Bosnia & Herzegovina 🇧🇦
+Botswana 🇧🇼
+Brazil 🇧🇷
+British Indian Ocean Territory 🇮🇴
+British Virgin Islands 🇻🇬
+Brunei 🇧🇳
+Bulgaria 🇧🇬
+Burkina Faso 🇧🇫
+Burundi 🇧🇮
+Cape Verde 🇨🇻
+Cambodia 🇰🇭
+Cameroon 🇨🇲
+Canada 🇨🇦
+Canary Islands 🇮🇨
+Cayman Islands 🇰🇾
+Central African Republic 🇨🇫
+Chad 🇹🇩
+Chile 🇨🇱
+China 🇨🇳
+Christmas Island 🇨🇽
+Cocos (Keeling) Islands 🇨🇨
+Colombia 🇨🇴
+Comoros 🇰🇲
+Congo - Brazzaville 🇨🇬
+Congo - Kinshasa 🇨🇩
+Cook Islands 🇨🇰
+Costa Rica 🇨🇷
+Côte d’Ivoire 🇨🇮
+Croatia 🇭🇷
+Cuba 🇨🇺
+Curaçao 🇨🇼
+Cyprus 🇨🇾
+Czech Republic 🇨🇿
+Denmark 🇩🇰
+Djibouti 🇩🇯
+Dominica 🇩🇲
+Dominican Republic 🇩🇴
+Ecuador 🇪🇨
+Egypt 🇪🇬
+El Salvador 🇸🇻
+Equatorial Guinea 🇬🇶
+Eritrea 🇪🇷
+Estonia 🇪🇪
+Ethiopia 🇪🇹
+European Union 🇪🇺
+Falkland Islands 🇫🇰
+Faroe Islands 🇫🇴
+Fiji 🇫🇯
+Finland 🇫🇮
+France 🇫🇷
+French Guiana 🇬🇫
+French Polynesia 🇵🇫
+French Southern Territories 🇹🇫
+Gabon 🇬🇦
+Gambia 🇬🇲
+Georgia 🇬🇪
+Germany 🇩🇪
+Ghana 🇬🇭
+Gibraltar 🇬🇮
+Greece 🇬🇷
+Greenland 🇬🇱
+Grenada 🇬🇩
+Guadeloupe 🇬🇵
+Guam 🇬🇺
+Guatemala 🇬🇹
+Guernsey 🇬🇬
+Guinea 🇬🇳
+Guinea-Bissau 🇬🇼
+Guyana 🇬🇾
+Haiti 🇭🇹
+Honduras 🇭🇳
+Hong Kong SAR China 🇭🇰
+Hungary 🇭🇺
+Iceland 🇮🇸
+India 🇮🇳
+Indonesia 🇮🇩
+Iran 🇮🇷
+Iraq 🇮🇶
+Ireland 🇮🇪
+Isle of Man 🇮🇲
+Israel 🇮🇱
+Italy 🇮🇹
+Jamaica 🇯🇲
+Japan 🇯🇵
+crossed flags 🎌
+Jersey 🇯🇪
+Jordan 🇯🇴
+Kazakhstan 🇰🇿
+Kenya 🇰🇪
+Kiribati 🇰🇮
+Kosovo 🇽🇰
+Kuwait 🇰🇼
+Kyrgyzstan 🇰🇬
+Laos 🇱🇦
+Latvia 🇱🇻
+Lebanon 🇱🇧
+Lesotho 🇱🇸
+Liberia 🇱🇷
+Libya 🇱🇾
+Liechtenstein 🇱🇮
+Lithuania 🇱🇹
+Luxembourg 🇱🇺
+Macau SAR China 🇲🇴
+Macedonia 🇲🇰
+Madagascar 🇲🇬
+Malawi 🇲🇼
+Malaysia 🇲🇾
+Maldives 🇲🇻
+Mali 🇲🇱
+Malta 🇲🇹
+Marshall Islands 🇲🇭
+Martinique 🇲🇶
+Mauritania 🇲🇷
+Mauritius 🇲🇺
+Mayotte 🇾🇹
+Mexico 🇲🇽
+Micronesia 🇫🇲
+Moldova 🇲🇩
+Monaco 🇲🇨
+Mongolia 🇲🇳
+Montenegro 🇲🇪
+Montserrat 🇲🇸
+Morocco 🇲🇦
+Mozambique 🇲🇿
+Myanmar (Burma) 🇲🇲
+Namibia 🇳🇦
+Nauru 🇳🇷
+Nepal 🇳🇵
+Netherlands 🇳🇱
+New Caledonia 🇳🇨
+New Zealand 🇳🇿
+Nicaragua 🇳🇮
+Niger 🇳🇪
+Nigeria 🇳🇬
+Niue 🇳🇺
+Norfolk Island 🇳🇫
+Northern Mariana Islands 🇲🇵
+North Korea 🇰🇵
+Norway 🇳🇴
+Oman 🇴🇲
+Pakistan 🇵🇰
+Palau 🇵🇼
+Palestinian Territories 🇵🇸
+Panama 🇵🇦
+Papua New Guinea 🇵🇬
+Paraguay 🇵🇾
+Peru 🇵🇪
+Philippines 🇵🇭
+Pitcairn Islands 🇵🇳
+Poland 🇵🇱
+Portugal 🇵🇹
+Puerto Rico 🇵🇷
+Qatar 🇶🇦
+Réunion 🇷🇪
+Romania 🇷🇴
+Russia 🇷🇺
+Rwanda 🇷🇼
+St. Barthélemy 🇧🇱
+St. Helena 🇸🇭
+St. Kitts & Nevis 🇰🇳
+St. Lucia 🇱🇨
+St. Pierre & Miquelon 🇵🇲
+St. Vincent & Grenadines 🇻🇨
+Samoa 🇼🇸
+San Marino 🇸🇲
+São Tomé & Príncipe 🇸🇹
+Saudi Arabia 🇸🇦
+Senegal 🇸🇳
+Serbia 🇷🇸
+Seychelles 🇸🇨
+Sierra Leone 🇸🇱
+Singapore 🇸🇬
+Sint Maarten 🇸🇽
+Slovakia 🇸🇰
+Slovenia 🇸🇮
+Solomon Islands 🇸🇧
+Somalia 🇸🇴
+South Africa 🇿🇦
+South Georgia & South Sandwich Islands 🇬🇸
+South Korea 🇰🇷
+South Sudan 🇸🇸
+Spain 🇪🇸
+Sri Lanka 🇱🇰
+Sudan 🇸🇩
+Suriname 🇸🇷
+Swaziland 🇸🇿
+Sweden 🇸🇪
+Switzerland 🇨🇭
+Syria 🇸🇾
+Taiwan 🇹🇼
+Tajikistan 🇹🇯
+Tanzania 🇹🇿
+Thailand 🇹🇭
+Timor-Leste 🇹🇱
+Togo 🇹🇬
+Tokelau 🇹🇰
+Tonga 🇹🇴
+Trinidad & Tobago 🇹🇹
+Tunisia 🇹🇳
+Turkey 🇹🇷
+Turkmenistan 🇹🇲
+Turks & Caicos Islands 🇹🇨
+Tuvalu 🇹🇻
+Uganda 🇺🇬
+Ukraine 🇺🇦
+United Arab Emirates 🇦🇪
+United Kingdom 🇬🇧
+United States 🇺🇸
+U.S. Virgin Islands 🇻🇮
+Uruguay 🇺🇾
+Uzbekistan 🇺🇿
+Vanuatu 🇻🇺
+Vatican City 🇻🇦
+Venezuela 🇻🇪
+Vietnam 🇻🇳
+Wallis & Futuna 🇼🇫
+Western Sahara 🇪🇭
+Yemen 🇾🇪
+Zambia 🇿🇲
+Zimbabwe 🇿🇼
+'
diff --git a/bin/min b/bin/min
new file mode 100755
index 0000000..715f4d1
--- /dev/null
+++ b/bin/min
@@ -0,0 +1,85 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ min NUMBER...
+ min -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Get the minimun number from the given values.
+
+
+ Examples:
+
+ Get the minimum number from a list:
+
+ $ min 5 3 9 9 4
+ 3
+
+ Get the minimum number when negative numbers are given
+
+ $ min -- -3 -5
+ -5
+
+ Get the minimum number given a single number
+
+ $ min 8
+ 8
+
+ The minimum default number:
+
+ $ min
+ 0
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${1:-}" ]; then
+ echo 0
+ exit
+fi
+
+N="$1"
+for n in "$@"; do
+ N=$((N < n ? N : n))
+done
+echo "$N"
diff --git a/bin/mkdtemp b/bin/mkdtemp
new file mode 100755
index 0000000..3729dd4
--- /dev/null
+++ b/bin/mkdtemp
@@ -0,0 +1,64 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ mkdtemp
+ mkdtemp -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Create a new temporary file and echo its name back.
+
+
+ Examples:
+
+ `cd` into temporary directory:
+
+ $ cd "$(mkdtemp)"
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+name="$(tmpname)"
+mkdir "$name"
+echo "$name"
diff --git a/bin/mkstemp b/bin/mkstemp
new file mode 100755
index 0000000..4097e59
--- /dev/null
+++ b/bin/mkstemp
@@ -0,0 +1,64 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ mkstemp
+ mkstemp -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Create a new temporary file and echo its name back.
+
+
+ Examples:
+
+ Capture output into temporary file:
+
+ $ OUT="$(mkstemp)"; cmd > "$OUT"
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+name="$(tmpname)"
+touch "$name"
+echo "$name"
diff --git a/bin/msg b/bin/msg
new file mode 100755
index 0000000..b2a6794
--- /dev/null
+++ b/bin/msg
@@ -0,0 +1,153 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ msg [-0|-1] [-X|-s|-S|-m|-D|-b] [MESSAGE]
+ msg -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -X send MESSAGE using the `xmpp` command
+ -s play $XDG_DATA_HOME/msg/{good,bad}.ogg sound
+ -S say MESSAGE using `speak`
+ -m send email with MESSAGE as subject and empty body
+ -D send desktop MESSAGE via `notify-send`
+ -b print terminal bell
+ -0 an OK message
+ -1 an error message
+ -h, --help show this message
+
+ MESSAGE the text to be sent by the relevant channel
+
+ Examples:
+
+ Ring a terminal bell and play a sound, representing an error:
+
+ $ msg -1sb
+
+ Send an email and an XMPP message:
+
+ $ msg -mX 'The message goes here'
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+sound() {
+ if [ "$OK" = true ]; then
+ play "$XDG_DATA_HOME"/msg/good.ogg 2>/dev/null
+ else
+ play "$XDG_DATA_HOME"/msg/bad.ogg 2>/dev/null
+ fi
+}
+
+OK=true
+XMPP=false
+SOUND=false
+SPEAK=false
+MAIL=false
+DESKTOP=false
+BELL=false
+ACTION_DONE=false
+while getopts '01XsSmDbh' flag; do
+ case "$flag" in
+ 0)
+ OK=true
+ ;;
+ 1)
+ OK=false
+ ;;
+ X)
+ XMPP=true
+ ACTION_DONE=true
+ ;;
+ s)
+ SOUND=true
+ ACTION_DONE=true
+ ;;
+ S)
+ SPEAK=true
+ ACTION_DONE=true
+ ;;
+ m)
+ MAIL=true
+ ACTION_DONE=true
+ ;;
+ D)
+ DESKTOP=true
+ ACTION_DONE=true
+ ;;
+ b)
+ BELL=true
+ ACTION_DONE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ "$ACTION_DONE" = false ]; then
+ sound
+ usage
+ help
+ exit
+fi
+
+
+MESSAGE="${1:-}"
+
+if [ "$XMPP" = true ]; then
+ eval "$(assert-arg "$MESSAGE" '-X MESSAGE')"
+ xmpp -m "$MESSAGE" eu@euandreh.xyz &
+fi
+if [ "$SOUND" = true ]; then
+ sound &
+fi
+if [ "$SPEAK" = true ]; then
+ eval "$(assert-arg "$MESSAGE" '-S MESSAGE')"
+ echo "$MESSAGE" | speak -v pt-BR &
+fi
+if [ "$MAIL" = true ]; then
+ eval "$(assert-arg "$MESSAGE" '-m MESSAGE')"
+ echo " " | email -s "$MESSAGE" eu@euandre.org &
+fi
+if [ "$DESKTOP" = true ]; then
+ eval "$(assert-arg "$MESSAGE" '-D MESSAGE')"
+ if [ "$OK" = true ]; then
+ notify-send -t 5000 "$MESSAGE" &
+ else
+ notify-send -t 5000 -u critical "$MESSAGE" &
+ fi
+fi
+if [ "$BELL" = true ]; then
+ printf '\a' &
+fi
+
+wait
diff --git a/bin/n-times b/bin/n-times
new file mode 100755
index 0000000..4fa8b96
--- /dev/null
+++ b/bin/n-times
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ n-times COUNT -- COMMAND...
+ n-times -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ COUNT the number of times for COMMAND to be executed
+
+
+ Examples:
+
+ Print 123 5 times:
+
+ $ n-times 5 echo 123
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+COUNT="${1:-}"
+shift
+
+eval "$(assert-arg "$COUNT" 'COUNT')"
+
+
+while true; do
+ if [ "$COUNT" = 0 ]; then
+ break
+ fi
+ COUNT=$((COUNT - 1))
+ "$@"
+done
diff --git a/bin/nato b/bin/nato
new file mode 100755
index 0000000..a9c21bf
--- /dev/null
+++ b/bin/nato
@@ -0,0 +1,102 @@
+#!/usr/bin/env perl
+
+use v5.34;
+use warnings;
+use feature 'signatures';
+no warnings ('experimental::signatures');
+use Getopt::Std ();
+
+sub usage($fh) {
+ print $fh <<~'EOF'
+ Usage:
+ nato
+ nato -h
+ EOF
+}
+
+sub help($fh) {
+ print $fh <<~'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Translate the given input to the NATO phonetic alphabet.
+
+
+ Examples:
+
+ Spell 'EuAndreh':
+
+ $ echo 'EuAndreh' | nato
+ Echo Uniform Alfa November Delta Romeo Echo Hotel
+ EOF
+}
+
+for (@ARGV) {
+ last if $_ eq '--';
+ if ($_ eq '--help') {
+ usage *STDOUT;
+ help *STDOUT;
+ exit
+ }
+}
+
+my %opts;
+if (!Getopt::Std::getopts('h', \%opts)) {
+ usage *STDERR;
+ exit 2;
+}
+
+if ($opts{h}) {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+}
+
+my %DICT = (
+ 'a' => 'Alfa',
+ 'b' => 'Bravo',
+ 'c' => 'Charlie',
+ 'd' => 'Delta',
+ 'e' => 'Echo',
+ 'f' => 'Foxtrot',
+ 'g' => 'Golf',
+ 'h' => 'Hotel',
+ 'i' => 'India',
+ 'j' => 'Juliett',
+ 'k' => 'Kilo',
+ 'l' => 'Lima',
+ 'm' => 'Mike',
+ 'n' => 'November',
+ 'o' => 'Oscar',
+ 'p' => 'Papa',
+ 'q' => 'Quebec',
+ 'r' => 'Romeo',
+ 's' => 'Sierra',
+ 't' => 'Tango',
+ 'u' => 'Uniform',
+ 'v' => 'Victor',
+ 'w' => 'Whiskey',
+ 'x' => 'X-ray',
+ 'y' => 'Yankee',
+ 'z' => 'Zulu',
+ '1' => 'One',
+ '2' => 'Two',
+ '3' => 'Three',
+ '4' => 'Four',
+ '5' => 'Five',
+ '6' => 'Six',
+ '7' => 'Seven',
+ '8' => 'Eight',
+ '9' => 'Nine',
+ '0' => 'Zero',
+);
+
+while (<STDIN>) {
+ for my $c (split //, $_) {
+ my $char = $DICT{lc $c};
+ print "$char " if defined $char;
+ }
+ print "\n";
+}
diff --git a/bin/ootb b/bin/ootb
new file mode 100755
index 0000000..3aa58db
--- /dev/null
+++ b/bin/ootb
@@ -0,0 +1,103 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ ootb BUILD_DIRECTORY < FILE...
+ ootb -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ BUILD_DIRECTORY the path of the build directory
+ FILE the files to be linked
+
+
+ Create a directory out of symlinks of the given files.
+
+ The goal is to enable parallel build directories to coexist,
+ so that one do *O*ut *O*f *T*ree *B*uilds without requiring the
+ build system or the project to explicitly support it.
+
+ If a repository contains the files:
+
+ .git/
+ Makefile
+ README.md
+ src/
+ file1.ext
+ file2.ext
+
+ Running `git ls-files | ootb build-1/` would create the
+ 'build-1/' directory with:
+
+ build-1/
+ Makefile -> /absolute/path/to/Makefile
+ README.md -> /absolute/path/to/README.md
+ src/
+ file1.ext -> /absolute/path/to/file1.ext
+ file2.ext -> /absolute/path/to/file2.ext
+
+ With that one can `cd build-1/` and run builds there, without
+ the build artifacts littering the source tree. Also, one could
+ create a build-2/ directory, where different compiler flags or
+ build options are given, such as debug/release, while sharing the
+ underlying source code.
+
+
+ Examples:
+
+ Create a 'build/' directory with the files from the Git repository:
+
+ $ git ls-files | ootb build/
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+BUILD_DIRECTORY="${1:-}"
+eval "$(assert-arg "$BUILD_DIRECTORY" 'BUILD_DIRECTORY')"
+mkdir -p "$BUILD_DIRECTORY"
+
+
+while read -r f; do
+ mkdir -p "$BUILD_DIRECTORY"/"$(dirname "$f")"
+ ln -fs "$PWD"/"$f" "$BUILD_DIRECTORY"/"$f"
+done
diff --git a/bin/open b/bin/open
new file mode 100755
index 0000000..d2eedd3
--- /dev/null
+++ b/bin/open
@@ -0,0 +1,101 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ open FILE...
+ open -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ Examples:
+
+ Open an HTML file on the current $BROWSER:
+ $ open index.html
+
+ Open multiple PDF files (with zathura):
+ $ open *.pdf
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${1:-}" ]; then
+ usage >&2
+ exit 2
+fi
+
+for f in "$@"; do
+ case "$f" in
+ *.ico|*.jpg|*.jpeg|*.png)
+ feh "$f"
+ ;;
+ https://www.youtube.com/watch*)
+ nohup mpv "$f" 1>&2 2>/dev/null &
+ ;;
+ *.flac|*.ogg|*.mkv|*.avi|*.mp4)
+ nohup mpv "$f" 1>&2 2>/dev/null &
+ ;;
+ http*|*.svg|*.html)
+ "$BROWSER" "$f"
+ ;;
+ gopher://*)
+ amfora "$f"
+ ;;
+ gemini://*)
+ telescope "$f"
+ ;;
+ *.pdf|*.djvu|*.ps|*.epub)
+ nohup zathura "$f" 1>&2 2>/dev/null &
+ ;;
+ *.txt)
+ less "$f"
+ ;;
+ *.midi)
+ timidity "$f"
+ ;;
+ mailto:*)
+ alot compose "$f"
+ ;;
+ *)
+ DIR="$(cd -- "$(dirname -- "$0")"; pwd)"
+ CMD="$(without-env PATH "$DIR" -- command -v xdg-open)"
+ "$CMD" "$f"
+ ;;
+ esac
+done
diff --git a/bin/player b/bin/player
new file mode 100755
index 0000000..b6e66d7
--- /dev/null
+++ b/bin/player
@@ -0,0 +1,136 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ player ACTION
+ player -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - backward: go back 5 seconds
+ - forward: go forward 5 seconds
+ - previous: go to the previous track
+ - next: go to the next track
+ - play-pause: play/pause
+ - rotate: rotate across available MPRIS players
+ - current: show the current MPRIS player
+
+
+ Manipulate the MPRIS audio player.
+
+
+ Examples:
+
+ Change the current MPRIS player:
+
+ $ player current
+
+
+ Play/pause:
+
+ $ player play-pause
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'P:h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+ACTION="${1:-}"
+
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+
+
+CURRENT_PLAYER_PATH="$XDG_CACHE_HOME"/euandreh-mpris-player.txt
+CURRENT_PLAYER="$(cat "$CURRENT_PLAYER_PATH" ||:)"
+AVAILABLE_PLAYERS="$(playerctl --list-all | LANG=POSIX.UTF-8 sort)"
+
+pick_first() {
+ echo "$AVAILABLE_PLAYERS" | head -n1
+}
+
+next_player() {
+ if [ -z "$CURRENT_PLAYER" ]; then
+ pick_first
+ elif ! echo "$AVAILABLE_PLAYERS" | grep -q "$CURRENT_PLAYER"; then
+ # Unknown $CURRENT_PLAYER, pick anyone
+ pick_first
+ else
+ INDEX="$(echo "$AVAILABLE_PLAYERS" | grep -n "$CURRENT_PLAYER" | cut -d: -f1)"
+ LENGTH="$(echo "$AVAILABLE_PLAYERS" | wc -l)"
+ if [ "$INDEX" = "$LENGTH" ]; then
+ # Reached the end of the $AVAILABLE_PLAYERS list, wrapping
+ pick_first
+ else
+ # Get the next player instead
+ echo "$AVAILABLE_PLAYERS" | awk -v idx="$INDEX" 'NR == idx+1 {print}'
+ fi
+ fi
+}
+
+case "$ACTION" in
+ backward)
+ playerctl --player="$CURRENT_PLAYER" position 5-
+ ;;
+ forward)
+ playerctl --player="$CURRENT_PLAYER" position 5+
+ ;;
+ previous)
+ playerctl --player="$CURRENT_PLAYER" previous
+ ;;
+ next)
+ playerctl --player="$CURRENT_PLAYER" next
+ ;;
+ play-pause)
+ playerctl --player="$CURRENT_PLAYER" play-pause
+ ;;
+ rotate)
+ PLAYER="$(next_player)"
+ echo "$PLAYER" > "$CURRENT_PLAYER_PATH"
+ notify-send -t 1000 "$PLAYER" 'current MPRIS target'
+ ;;
+ current)
+ printf '%s\n' "$CURRENT_PLAYER"
+ ;;
+ *)
+ printf 'Bad ACTION: "%s".\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/playlist b/bin/playlist
new file mode 100755
index 0000000..bd01d23
--- /dev/null
+++ b/bin/playlist
@@ -0,0 +1,103 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ playlist ACTION
+ playlist -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - prompt
+ - run
+
+
+ Manage the playlist.
+
+
+ Examples:
+
+ Enqueue a video:
+
+ $ playlist prompt
+
+
+ Play the next video in the queue:
+
+ $ playlist run
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+ACTION="${1:-}"
+
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+F="$XDG_DATA_HOME"/euandreh/playlist.txt
+
+prompt() {
+ ENTRY="$(zenity --text 'URL of the video to enqueue:' --entry ||:)"
+ if [ -n "$ENTRY" ]; then
+ echo "$ENTRY" >> "$F"
+ fi
+}
+
+run() {
+ next="$(head -n1 "$F")"
+ if [ -z "$next" ]; then
+ return
+ fi
+ mpv "$next"
+ echo "$next" >> "queue.$F"
+ tail -n+2 "$F" | sponge "$F"
+}
+
+case "$ACTION" in
+ prompt)
+ prompt
+ ;;
+ run)
+ run
+ ;;
+ *)
+ printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/pre b/bin/pre
new file mode 100755
index 0000000..2b32f8f
--- /dev/null
+++ b/bin/pre
@@ -0,0 +1,78 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ pre [-c COLOR] PREFIX
+ pre -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -c COLOR ANSI color to be used on the prefix text
+ -h, --help show this message
+
+ Prefix STDIN with PREFIX.
+
+
+ Examples:
+
+ Prefix with 'database':
+
+ $ ./run-db.sh | pre 'database'
+
+
+ Prefix with yellow 'numbers':
+
+ $ seq 10 | pre -c yellow numbers
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+COLOR=''
+while getopts 'c:h' flag; do
+ case "$flag" in
+ c)
+ COLOR="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+PREFIX="${1:-}"
+eval "$(assert-arg "$PREFIX" 'PREFIX')"
+
+while read -r line; do
+ if [ -z "$COLOR" ]; then
+ printf '%s: %s\n' "$PREFIX" "$line"
+ else
+ printf '%s: %s\n' "$(color -c "$COLOR" "$PREFIX")" "$line"
+ fi
+done
diff --git a/bin/print b/bin/print
new file mode 100755
index 0000000..e0d3d6e
--- /dev/null
+++ b/bin/print
@@ -0,0 +1,151 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ print [-d] [-q QUALITY] [FILE...]
+ print -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -d print duplex/double-sided
+ -q QUALITY choose the print quality, either:
+ low, medium (default) or high.
+ -h, --help show this message
+
+ Examples:
+
+ Print the given PostScript file with default quality:
+ $ print f1.ps
+
+ Print multiple PDF files with high quality:
+ $ print -dq high *.pdf
+
+ Print the file from STDIN, double-sided:
+ $ print -d < f2.ps
+
+ Print multiple source code files:
+ $ print src/*.{c,h}
+ EOF
+}
+
+mkdtemp() {
+ name="$(echo 'mkstemp(template)' |
+ m4 -D template="${TMPDIR:-/tmp}/m4-tmpname.")"
+ rm -f "$name"
+ mkdir "$name"
+ echo "$name"
+}
+
+n_pages() {
+ pdftk "$1" dump_data | awk '/NumberOfPages/ { print $2 }'
+}
+
+main() {
+ if file -b "$FILE" | grep -q PostScript; then
+ ps2pdf "$FILE" "$NEWDIR"/in.pdf
+ elif file -b "$FILE" | grep -q PDF; then
+ cp "$FILE" "$NEWDIR"/in.pdf
+ else
+ enscript -o- "$FILE" | ps2pdf - "$NEWDIR"/in.pdf
+ fi
+ cd "$NEWDIR"
+
+ if [ -z "$DUPLEX" ]; then
+ lp in.pdf
+ cd - > /dev/null
+ return
+ fi
+
+ if [ "$(n_pages in.pdf)" = '1' ]; then
+ lp in.pdf
+ return
+ fi
+
+ pdftk A=in.pdf cat Aodd output odd.pdf
+ pdftk A=in.pdf cat Aeven output even.pdf
+
+ NODD="$(n_pages odd.pdf)"
+ NEVEN="$(n_pages even.pdf)"
+
+ printf 'Printing odd pages...\n' >&2
+ lp odd.pdf
+ printf 'Has printing finished yet? Once it does, reload the pages and hit it enter to continue. '
+ read -r < /dev/tty
+ lp even.pdf
+
+ if [ "$NODD" != "$NEVEN" ]; then
+ printf '\n' | lp
+ fi
+
+ cd - > /dev/null
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+lpoptions -o PrintQuality=standard
+DUPLEX=
+while getopts 'dq:h' flag; do
+ case "$flag" in
+ d)
+ DUPLEX=1
+ ;;
+ q)
+ case "$OPTARG" in
+ low)
+ lpoptions -o PrintQuality=draft
+ ;;
+ medium)
+ lpoptions -o PrintQuality=standard
+ ;;
+ high)
+ lpoptions -o PrintQuality=high
+ ;;
+ *)
+ echo "Bad QUALITY option: \"$OPTARG\"" >&2
+ exit 2
+ ;;
+ esac
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+NEWDIR="$(mkdtemp)"
+if [ -z "${1:-}" ]; then
+ FILE="$NEWDIR"/STDIN
+ cat - > "$FILE"
+ main
+else
+ for f in "$@"; do
+ FILE="$f"
+ main
+ done
+fi
diff --git a/bin/prompt b/bin/prompt
new file mode 100755
index 0000000..247c81a
--- /dev/null
+++ b/bin/prompt
@@ -0,0 +1,76 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ prompt STRING|-
+ prompt -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ STRING the text to be displayed in the prompt
+
+
+ Display a prompt and return a value corresponding to the
+ response.
+
+
+ Examples:
+
+ Conditionally run download command
+
+ if prompt 'Download files?'; then
+ run_download;
+ fi
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+STRING="${1:-}"
+eval "$(assert-arg "$STRING" 'STRING')"
+
+printf '%s' "$STRING"
+printf ' [Y/n]: '
+read -r yesno
+if [ "$yesno" != 'n' ] && [ "$yesno" != 'N' ]; then
+ exit 0
+else
+ exit 1
+fi
diff --git a/bin/qr b/bin/qr
new file mode 100755
index 0000000..c0462e1
--- /dev/null
+++ b/bin/qr
@@ -0,0 +1,71 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ qr [-s PIXEL_SIZE]
+ qr -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -s PIXEL_SIZE size of the pixel (default 10)
+ -h, --help show this help message
+
+
+ Read data from STDIN and present a QR image with said data.
+
+
+ Examples:
+
+ Link to my homepage:
+
+ $ printf 'https://euandre.org' | qr
+
+
+ Numbers with a smaller pixel size:
+
+ $ seq 99 | qr -s 5
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+PIXEL_SIZE=10
+while getopts 's:h' flag; do
+ case "$flag" in
+ s)
+ PIXEL_SIZE="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+cat | qrencode -s "$PIXEL_SIZE" -o- | feh -
diff --git a/bin/repos b/bin/repos
new file mode 100755
index 0000000..e45f4c8
--- /dev/null
+++ b/bin/repos
@@ -0,0 +1,175 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ repos [-e DIR...] [-v] [DIRECTORY]
+ repos -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -e DIR exclude the given directory from traversing
+ -v enable verbose mode
+ -h, --help show this message
+
+ DIRECTORY the folder to traverse
+
+
+ Traverse DIRECTORY looking for VCS repositores. As soon as
+ one is found, stop recursing, and emit its name alongside its
+ type.
+
+ On verbose mode, print the directories being visited.
+
+
+ Examples:
+
+ Show all repositories under ~/dev/, excluding a few directories:
+
+ $ repos -e ~/dev/go/ -e ~/dev/quicklisp/ ~/dev/
+ # ...
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+
+# Similar to urlencode but only for % and \n (and not \0).
+array_encode() {
+ sed 's/%/%25/g' | sed -e :a -e '$!N; s/\n/%0A/; ta'
+}
+
+array_decode() {
+ sed -e 's/%0A/\
+/g' -e 's/%25/%/g'
+}
+
+
+#
+# CAVEAT:
+# To avoid needing to keep decoding the array elements on every call to
+# `arr_includes`, assume directory names don't contain newlines. This makes the
+# current code scanning ~/dev/ from 8 seconds go to 1 second.
+#
+
+arr_push() {
+ ARR="$1"
+ ELT="$2"
+ if [ -n "$ARR" ]; then
+ echo "$ARR"
+ fi
+ echo "$ELT" # | array_encode (see CAVEAT)
+}
+
+arr_includes() {
+ ARR="$1"
+ ELT="$2"
+ echo "$ARR" | while read -r el; do
+ # if [ "$(printf '%s\n' "$el" | array_decode)" = "$ELT" ]; then (see CAVEAT)
+ if [ "$el" = "$ELT" ]; then
+ return 2
+ fi
+ done
+ if [ $? = 2 ]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+EXCLUDE=
+VERBOSE=false
+while getopts 'e:vh' flag; do
+ case "$flag" in
+ e)
+ case "$OPTARG" in
+ */)
+ ARG="$OPTARG"
+ ;;
+ *)
+ ARG="$OPTARG/"
+ ;;
+ esac
+ EXCLUDE="$(arr_push "$EXCLUDE" "$ARG")"
+ ;;
+ v)
+ VERBOSE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+is_repository() {
+ TYPE="$(vcs -C "$1" -t 2>/dev/null)"
+ if [ -n "$TYPE" ]; then
+ echo "$1"
+ else
+ return 1
+ fi
+}
+
+
+traverse_directory() {
+ if [ "$VERBOSE" = true ]; then
+ printf 'cur: %s\n' "$1" >&2
+ fi
+ if arr_includes "$EXCLUDE" "$1"; then
+ return
+ fi
+ if is_repository "$1"; then
+ return
+ fi
+ for d in "$1"/*; do
+ if [ "$VERBOSE" = true ]; then
+ printf 'cur: %s\n' "$d" >&2
+ fi
+ if [ ! -d "$d" ]; then
+ continue
+ fi
+ if arr_includes "$EXCLUDE" "$d/"; then
+ continue
+ fi
+ if ! is_repository "$d"; then
+ traverse_directory "$d"
+ fi
+ done
+}
+
+if [ -z "${1:-}" ]; then
+ set -- "$PWD"
+fi
+
+for dir in "$@"; do
+ traverse_directory "${dir%%/}"
+done
diff --git a/bin/rfc b/bin/rfc
new file mode 100755
index 0000000..9c71ebc
--- /dev/null
+++ b/bin/rfc
@@ -0,0 +1,150 @@
+#!/bin/sh
+set -eu
+
+D="${XDG_DATA_HOME:-$HOME/.local/share}/doc/rfc"
+PROMPT_DB="$(
+ cat <<-EOF
+ RFC directory does not exist:
+ $D/
+
+ Do you want to download the files to create it?
+ EOF
+)"
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ rfc [-w] RFC_NUMBER
+ rfc -u
+ rfc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -w show the path to the RFC file instead of displaying
+ its contents
+ -u update the local RFC database
+ -h, --help show this message
+
+ Lookup the given RFC
+ in $XDG_DATA_HOME/doc/rfc/ (defaults to ~/.local/share),
+ and feed it into the $PAGER, akin to doing:
+
+ $ $PAGER $XDG_DATA_HOME/doc/rfc/rfc$RFC_NUMBER.txt
+
+ If the $XDG_DATA_HOME/doc/rfc/ directory doesn't exist, it gets
+ created it by downloading the latest RFC files and placing all .txt
+ files there.
+
+
+ Examples:
+
+
+ Show RFC 1234 in $PAGER:
+
+ $ rfc 1234
+
+
+ Print path to RFC 2222:
+
+ $ rfc 2222
+
+
+ Download the latest RFCs:
+
+ $ rfc -u
+ EOF
+}
+
+view() {
+ if [ -t 1 ]; then
+ ${PAGER:-cat}
+ else
+ cat
+ fi
+}
+
+update() {
+ rsync -avzP --delete ftp.rfc-editor.org::rfcs-text-only "$D"
+ STATUS=$?
+ if [ "$STATUS" != 0 ]; then
+ exit "$STATUS"
+ fi
+}
+
+check_local_db() {
+ if [ ! -e "$D" ]; then
+ if prompt "$PROMPT_DB"; then
+ update
+ else
+ echo 'No local RFC database to operate on.' >&2
+ exit 1
+ fi
+ fi
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'wuh' flag; do
+ case "$flag" in
+ w)
+ WHERE=true
+ ;;
+ u)
+ UPDATE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+RFC_NUMBER="${1:-}"
+F="$D/rfc${RFC_NUMBER}.txt"
+
+check_local_db
+
+if [ "${UPDATE:-}" = true ]; then
+ update
+ exit
+fi
+
+eval "$(assert-arg "$RFC_NUMBER" 'RFC_NUMBER')"
+
+if [ ! -e "$F" ]; then
+ printf 'Given RFC_NUMBER "%s" does not exist at:\n%s\n' \
+ "$RFC_NUMBER" "$F" >&2
+ exit 2
+fi
+
+if [ "${WHERE:-}" = true ]; then
+ printf '%s\n' "$F"
+ exit
+else
+ view < "$F"
+ exit
+fi
diff --git a/bin/serve b/bin/serve
new file mode 100755
index 0000000..4a06c50
--- /dev/null
+++ b/bin/serve
@@ -0,0 +1,78 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ serve [-d DIRECTORY] [-p PORT]
+ serve -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -d DIRECTORY the directory to serve (default: ".")
+ -p PORT the port to listen on (default: 8000)
+ -h, --help show this message
+
+
+ Serve DIRECTORY via HTTP as a static file server, and open the
+ URL on the $BROWSER.
+
+
+ Examples:
+
+ Serve "." on the default PORT:
+
+ $ serve
+
+
+ Serve "public/" on port 1234:
+
+ $ serve -d public/ -p 1234
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+DIRECTORY='.'
+PORT=8000
+while getopts 'd:p:h' flag; do
+ case "$flag" in
+ d)
+ DIRECTORY="$OPTARG"
+ ;;
+ p)
+ PORT="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+open "http://localhost:$PORT"
+python3 -m http.server -d "$DIRECTORY" "$PORT"
diff --git a/bin/slugify b/bin/slugify
new file mode 100755
index 0000000..aa3b50a
--- /dev/null
+++ b/bin/slugify
@@ -0,0 +1,69 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ slugify < STDIN
+ slugify -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ "slugify" the input string, removing diacritics and punctuation
+ from the string, on a best-effort basis.
+
+
+ Examples:
+
+ Slugify the input string:
+
+ $ echo 'Saçi-pererê, tomando açaí!!' | slugify
+ saci-perere-tomando-acai
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ esac
+done
+shift $((OPTIND - 1))
+
+iconv -ct ASCII//TRANSLIT |
+ tr '[:upper:]' '[:lower:]' |
+ sed \
+ -e 's/[^a-z0-9]/-/g' \
+ -e 's/--*/-/g' \
+ -e 's/^-//' \
+ -e 's/-$//'
diff --git a/bin/status-bar b/bin/status-bar
new file mode 100755
index 0000000..bd55b05
--- /dev/null
+++ b/bin/status-bar
@@ -0,0 +1,104 @@
+#!/usr/bin/env perl
+
+#
+# Derived from:
+# https://github.com/i3/i3status/blob/28399bf84693a03eb772be647d5927011c1d2619/contrib/wrapper.pl
+#
+
+use v5.34;
+use warnings;
+use feature 'signatures';
+no warnings ('experimental::signatures');
+use Getopt::Std ();
+use JSON ();
+
+sub usage($fh) {
+ print $fh <<~'EOF'
+ Usage:
+ status-bar
+ status-bar -h
+ EOF
+}
+
+sub help($fh) {
+ print $fh <<~'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Process the output of i3status and add custom information for
+ showing in the i3 bar.
+
+
+ Examples:
+
+ Process i3status:
+
+ $ i3status | status-bar
+
+
+ Configure i3 to use status-bar:
+
+ # In $XDG_CONFIG_HOME/i3/config
+ bar {
+ status_command i3status | status-bar
+ }
+ EOF
+}
+
+for (@ARGV) {
+ last if $_ eq '--';
+ if ($_ eq '--help') {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+ }
+}
+
+my %opts;
+if (!Getopt::Std::getopts('h', \%opts)) {
+ usage *STDERR;
+ exit 2;
+}
+
+if ($opts{h}) {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+}
+
+# Don't buffer any output
+$| = 1;
+
+# Skip the first line which contains the version header.
+print scalar <STDIN>;
+
+# The second line contains the start of the infinite array.
+print scalar <STDIN>;
+
+# Read lines forever, ignore a comma at the beginning if it exists.
+while (my ($statusline) = (<STDIN> =~ /^,?(.*)/)) {
+ # Decode the JSON-encoded line.
+ my @blocks = @{JSON::decode_json($statusline)};
+
+ # Prefix our own information (you coud also suffix or insert in the
+ # middle).
+
+ my $mpris = `player current`;
+ chomp $mpris;
+
+ my $vms = `vm status | grep :up\$ | wc -l`;
+ chomp $vms;
+
+ @blocks = ({
+ full_text => $vms,
+ name => 'vms',
+ }, {
+ full_text => $mpris,
+ name => 'mpris',
+ }, @blocks);
+
+ # Output the line as JSON.
+ print JSON::encode_json(\@blocks) . ",\n";
+}
diff --git a/bin/stopwatch b/bin/stopwatch
new file mode 100755
index 0000000..3d5cd07
--- /dev/null
+++ b/bin/stopwatch
@@ -0,0 +1,65 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ stopwatch
+ stopwatch -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Run a TUI stopwatch.
+
+
+ Examples:
+
+ Just run it:
+
+ $ stopwatch
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+date "+%l:%M:%S%p: stopwatch started (^D to stop)"
+time cat
+date "+%l:%M:%S%p: stopwatch stopped"
diff --git a/bin/tmp b/bin/tmp
new file mode 100755
index 0000000..2dc0b48
--- /dev/null
+++ b/bin/tmp
@@ -0,0 +1,91 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ tmp FILE...
+ tmp -d
+ tmp -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -d delete the remote "tmp/" folder
+ -h, --help show this message
+
+
+ Copies a file to the public server.
+
+
+ Examples:
+
+ Copy f.txt:
+
+ $ tmp f.txt
+
+
+ Cleanup the $REMOTE:
+
+ $ tmp -d
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+REMOTE='euandreh.xyz'
+DIR='/opt/www/euandreh.xyz/static/tmp'
+while getopts 'dh' flag; do
+ case "$flag" in
+ d)
+ printf 'Deleting %s:%s...\n' "$REMOTE" "$DIR/" >&2
+ ssh "$REMOTE" rm -rf "$DIR"
+ exit
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+FILE="${1:-}"
+
+if [ -z "$FILE" ]; then
+ printf 'Missing FILE.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+for f in "$@"; do
+ FILENAME="$(basename "$f")"
+ # shellcheck disable=2029
+ ssh "$REMOTE" "mkdir -p '$DIR' && cat > '$DIR/$FILENAME'" < "$f"
+
+ LINK="$(printf 'https://%s/tmp/%s' "$REMOTE" "$FILENAME")"
+ open "$LINK"
+ if [ $# = 1 ]; then
+ printf '%s' "$LINK" | copy
+ printf 'Copied %s to the clipboard!\n' "$LINK" >&2
+ fi
+done
diff --git a/bin/tmpname b/bin/tmpname
new file mode 100755
index 0000000..89d7e4d
--- /dev/null
+++ b/bin/tmpname
@@ -0,0 +1,66 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ tmpname
+ tmpname -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Generate a temporary name.
+
+
+ Examples:
+
+ Create a temporary file:
+
+ $ OUT="$(tmpname)"; touch "$OUT"; cmd > "$OUT"
+
+
+ `cd` into a temporary directory:
+
+ $ DIR="$(tmpname)"; mkdir -p "$DIR"; cd "$DIR"
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+echo "${TMPDIR:-/tmp}/uuid-tmpname with spaces.$(uuid)"
diff --git a/bin/tuivid b/bin/tuivid
new file mode 100755
index 0000000..54bddcf
--- /dev/null
+++ b/bin/tuivid
@@ -0,0 +1,65 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ tuivid FILE
+ tuivid -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ FILE the name of the video file
+
+
+ Play a video in the terminal, withut X or a GUI.
+
+
+ Examples:
+
+ Play "movie.mp4":
+
+ $ tuivid movie.mp4
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+exec mpv --quiet --vo=tct --vo-tct-256=yes --vo-tct-algo=plain --framedrop=vo "$@"
diff --git a/bin/uc b/bin/uc
new file mode 100755
index 0000000..e8bd9fb
--- /dev/null
+++ b/bin/uc
@@ -0,0 +1,70 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ uc
+ uc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Transforms text from STDIN from upper-case to lower-case. It is
+ the equivalent of running 'tr [:lower:] [:upper:]'.
+
+
+ Examples:
+
+ Normalize to lower-case:
+
+ $ echo EuAndreh | uc
+ EUANDREH
+
+
+ Keep the text as-is:
+
+ $ echo ANDREH | uc
+ ANDREH
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+tr '[:lower:]' '[:upper:]'
diff --git a/bin/untill b/bin/untill
new file mode 100755
index 0000000..030f921
--- /dev/null
+++ b/bin/untill
@@ -0,0 +1,83 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ until [-n SECONDS] -- COMMAND...
+ until -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -n SECONDS the amount of seconds to sleep between
+ attempts (default: 5)
+ -h, --help show this message
+
+
+ Runs COMMAND until it eventually succeeds. Sleep SECONDS
+ between attempts.
+
+
+ Examples:
+
+ Try flaky build until it succeeds:
+
+ $ until guix home build home.scm
+
+
+ Try to build until a new version is successfull,
+ waiting 30 seconds between attempts:
+
+ $ until -n 30 -- x git pull AND make
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+_SECONDS=5
+while getopts 'n:h' flag; do
+ case "$flag" in
+ n)
+ _SECONDS="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+ATTEMPT=1
+while true; do
+ printf 'Attempt %s.\n' "$ATTEMPT" >&2
+ ATTEMPT=$((ATTEMPT + 1))
+ if "$@"; then
+ break
+ fi
+ sleep "$_SECONDS"
+done
diff --git a/bin/update b/bin/update
new file mode 100755
index 0000000..cc2412e
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ update
+ update -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Updates miscellaneous things on the workstation:
+ - "guix pull" on the "andreh" and "root" accounts;
+ - get latest RFCs;
+ - updates RSS feeds;
+ - updates source code repositories.
+
+
+ Examples:
+
+ Just use it:
+
+ $ update
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+guix pull
+rfc -u
+
+repos -e ~/dev/go/ -e ~/dev/quicklisp/ -e ~/dev/archive/ ~/dev/ |
+ xargs -I% -P4 x \
+ echo 'Fetching on %.' AND \
+ vcs -C% fetch OR \
+ echo 'WARNING: Failed to fetch repository: %.' >&2
diff --git a/bin/upgrade b/bin/upgrade
new file mode 100755
index 0000000..4447a3d
--- /dev/null
+++ b/bin/upgrade
@@ -0,0 +1,66 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ upgrade
+ upgrade -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Upgrades the system:
+ - reconfigure the Guix "home" environment and "system".
+
+
+ Examples:
+
+ Just use it:
+
+ $ upgrade
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+pass show velhinho/0-andreh-password |
+ head -n1 |
+ sudo -ES guix system -v3 reconfigure /etc/guix/configuration.scm
+guix home -v3 reconfigure "$XDG_CONFIG_HOME"/guix/home.scm
diff --git a/bin/uri b/bin/uri
new file mode 100755
index 0000000..9b7a61b
--- /dev/null
+++ b/bin/uri
@@ -0,0 +1,75 @@
+#!/usr/bin/env perl
+
+use v5.34;
+use warnings;
+use feature 'signatures';
+no warnings ('experimental::signatures');
+use Getopt::Std ();
+use URI::Escape ();
+
+sub usage($fh) {
+ print $fh <<~'EOF'
+ Usage:
+ uri [-e|-d]
+ uri -h
+ EOF
+}
+
+sub help($fh) {
+ print $fh <<~'EOF'
+
+ Options:
+ -e encode the string (the default action)
+ -d decode the string
+ -h, --help show this message
+
+
+ Get a string from STDIN and convert it to/from URL encoding.
+
+
+ Examples:
+
+ Encode the URL:
+
+ $ echo "https://euandre.org/?q=$(printf '%s' 'a param' | uri)"
+ https://euandre.org/?q=a%20param
+
+
+ Decode the content from the file:
+
+ $ uri -d < file
+ EOF
+}
+
+for (@ARGV) {
+ last if $_ eq '--';
+ if ($_ eq '--help') {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+ }
+}
+
+my %opts;
+if (!Getopt::Std::getopts('edh', \%opts)) {
+ usage *STDERR;
+ exit 2;
+}
+
+if ($opts{h}) {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+} elsif ($opts{e} and $opts{d}) {
+ say STDERR 'Both -e and -d given. Pick one.';
+ say STDERR '';
+ usage *STDERR;
+ exit 2;
+}
+
+
+if ($opts{d}) {
+ print URI::Escape::uri_unescape(<STDIN>);
+} else {
+ print URI::Escape::uri_escape(<STDIN>);
+}
diff --git a/bin/uuid b/bin/uuid
new file mode 100755
index 0000000..34b685f
--- /dev/null
+++ b/bin/uuid
@@ -0,0 +1,62 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ uuid
+ uuid -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+
+ Generate UUID from /dev/random.
+
+
+ Examples:
+
+ Generate a UUID:
+
+ $ uuid
+ 755244c8-f955-16df-75cc-f25600c90422
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+od -xN20 /dev/random |
+ awk 'NR == 1 { OFS="-"; print $2$3,$4,$5,$6,$7$8$9; exit }'
diff --git a/bin/vcs b/bin/vcs
new file mode 100755
index 0000000..33bae3c
--- /dev/null
+++ b/bin/vcs
@@ -0,0 +1,255 @@
+#!/bin/sh
+set -eu
+
+
+TYPES='
+git
+mercurial
+bitkeeper
+darcs
+cvs
+fossil
+'
+
+
+guess_type() {
+ if [ -e "$1"/.git ]; then
+ echo git
+ elif [ -e "$1"/.hg ]; then
+ echo mercurial
+ elif [ -e "$1"/.bk ]; then
+ echo bitkeeper
+ elif [ -e "$1"/_darcs ]; then
+ echo darcs
+ elif [ -e "$1"/CVS/ ]; then
+ echo cvs
+ elif [ -e "$1"/.fslckout ]; then
+ echo fossil
+ else
+ return 1
+ fi
+}
+
+
+
+
+git_fetch() {
+ git fetch "$@"
+}
+
+darcs_fetch() {
+ darcs fetch "$@"
+}
+
+mercurial_fetch() {
+ hg pull "$@"
+}
+
+fossil_fetch() {
+ fossil pull "$@"
+}
+
+cvs_fetch() {
+ timeout 300 cvs update "$@"
+}
+
+git_status() {
+ git status "$@"
+}
+
+fossil_status() {
+ fossil status "$@"
+}
+
+git_diff() {
+ git diff "$@"
+}
+
+git_ps1() {
+ BRANCH_NAME="$(git rev-parse --abbrev-ref HEAD)"
+ OUT="$(git status --short --branch --porcelain)"
+ BRANCH_LINE="$(echo "$OUT" | head -n 1)"
+ DIFF_LINES="$(echo "$OUT" | tail -n +2)"
+
+ IS_AHEAD=false
+ IS_BEHIND=false
+ if echo "$BRANCH_LINE" | grep -q 'ahead'; then
+ IS_AHEAD=true
+ fi
+ if echo "$BRANCH_LINE" | grep -q 'behind'; then
+ IS_BEHIND=true
+ fi
+
+ LINE=''
+
+ if [ "$IS_AHEAD" = true ] && [ "$IS_BEHIND" = true ]; then
+ LINE="^^^ $BRANCH_NAME vvv"
+ elif [ "$IS_AHEAD" = true ]; then
+ LINE="^ $BRANCH_NAME ^"
+ elif [ "$IS_BEHIND" = true ]; then
+ LINE="v $BRANCH_NAME v"
+ else
+ LINE="$BRANCH_NAME"
+ fi
+
+ HAS_DIFF=false
+ HAS_UNTRACKED=false
+ if echo "$DIFF_LINES" | grep -q '^[A|D|M| ][M|D| ]'; then
+ HAS_DIFF=true
+ fi
+ if echo "$DIFF_LINES" | grep -q '^[?][?]'; then
+ HAS_UNTRACKED=true
+ fi
+
+ if [ "$HAS_DIFF" = true ]; then
+ COLOR_FN=redb
+ LINE="{$LINE}"
+ elif [ "$IS_AHEAD" = true ] || [ "$IS_BEHIND" = true ]; then
+ COLOR_FN=bluei
+ LINE="[$LINE]"
+ elif [ "$HAS_UNTRACKED" = true ]; then
+ COLOR_FN=lightblue
+ LINE="{$LINE}"
+ else
+ COLOR_FN=green
+ LINE="($LINE)"
+ fi
+
+ color -c "$COLOR_FN" "$LINE"
+
+ BRANCH_COUNT="$(git branch --list | wc -l)"
+ if [ "$BRANCH_COUNT" -gt 1 ]; then
+ color -c lightblue "<$BRANCH_COUNT>"
+ fi
+
+ STASH_COUNT="$(git stash list | wc -l)"
+ if [ "$STASH_COUNT" != 0 ]; then
+ color -c red "*$STASH_COUNT"
+ fi
+
+ color -c blacki " - git/$(git rev-parse HEAD)"
+}
+
+fossil_ps1() {
+ BRANCH_NAME="$(fossil branch current)"
+
+ if [ -n "$(fossil extras)" ]; then
+ HAS_UNTRACKED=1
+ fi
+
+ BRANCH_MARKER="$BRANCH_NAME"
+
+ if [ -n "${HAS_UNTRACKED:-}" ]; then
+ COLOR_FN=lightblue
+ LINE="($BRANCH_MARKER)"
+ else
+ COLOR_FN=green
+ LINE="($BRANCH_MARKER)"
+ fi
+
+ color -c "$COLOR_FN" "$LINE"
+
+ color -c blacki " - fossil/$(fossil info | awk '/^checkout:/ { print $2 }')"
+}
+
+mercurial_ps1() {
+ BRANCH_NAME="$(hg branch)"
+}
+
+git_gc() {
+ git remote prune origin "$@"
+}
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ vcs [-C DIR] ACTION
+ vcs [-C DIR] -l|-t
+ vcs -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -C DIR change to DIR instead of PWD
+ -l list the supported VCS types
+ -t show the guesses VCS type
+ -h, --help show this message
+
+ ACTION the action to be performed on the repository:
+ - fetch
+ - ps1
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'C:lth' flag; do
+ case "$flag" in
+ C)
+ cd "$OPTARG"
+ ;;
+ l)
+ # shellcheck disable=2086
+ echo $TYPES | tr ' ' '\n'
+ exit
+ ;;
+ t)
+ guess_type "$PWD" || {
+ printf 'Could not guess the type of the repository.\n' >&2
+ exit 2
+ }
+ exit
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ esac
+done
+shift $((OPTIND - 1))
+
+ACTION="${1:-}"
+eval "$(assert-arg "$ACTION" 'ACTION')"
+shift
+
+if [ "${1:-}" = '--' ]; then
+ shift
+fi
+
+TYPE="$(guess_type "$PWD")"
+if [ -z "$TYPE" ]; then
+ printf 'Could not guess the type of the repository.\n' >&2
+ exit 2
+fi
+
+CMD="$TYPE"_"$ACTION"
+if ! command -v "$CMD" >/dev/null; then
+ printf 'Invalid ACTION: "%s".\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+fi
+
+"$CMD" "$@"
diff --git a/bin/vm b/bin/vm
new file mode 100755
index 0000000..813f1e6
--- /dev/null
+++ b/bin/vm
@@ -0,0 +1,176 @@
+#!/bin/sh
+set -eu
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ vm ACTION [OS]
+ vm -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - up
+ - down
+ - status
+ OS the name of the OS to be acted upon
+
+
+ Manage the state of known virtual machines.
+
+ The VM QCOW2 images are stored under
+ $XDG_STATE_HOME/euandreh/qemu/, as "$OS.qcow2" files. The PIDs
+ of the running images are stored in individual files under
+ $XDG_RUNTIME_DIR/vm-pids/, as "$OS.pid" files.
+
+ It also generates an SSH configuration file to bu `Included`
+ by the main $XDG_CONFIG_HOME/ssh/config file, which contains
+ one alias entry for each VM, so that one can do a combination
+ of `vm up alpine && ssh alpine`.
+
+
+ Examples:
+
+ Start the VM for Alpine:
+
+ $ vm up alpine
+
+
+ Stop the VM for Slackware, which was already down
+
+ $ vm down slackware
+ The VM for "slackware" is not running, already.
+
+
+ List the available VMs, and their current state:
+
+ $ vm status
+ alpine:up
+ slackware:down
+ freebsd:up
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+ACTION="${1:-}"
+OS="${2:-}"
+
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+
+VMS='
+alpine:60022
+'
+
+for vm in $VMS; do
+ NAME="$(echo "$vm" | cut -d: -f1)"
+ PORT="$(echo "$vm" | cut -d: -f2)"
+ cat <<-EOF
+ Host $NAME
+ HostName localhost
+ Port $PORT
+
+ EOF
+done > "$XDG_DATA_HOME"/euandreh/vm-ssh.conf
+
+PIDS_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/vm-pids"
+PID_F="$PIDS_DIR/$OS.pid"
+mkdir -p "$PIDS_DIR"
+
+
+port_for_os() {
+ _OS="$1"
+ _PORT="$(echo "$VMS" | awk -F: -vOS="$_OS" '$1 == OS { print $2 }')"
+ if [ -z "$_PORT" ]; then
+ printf 'Unknown OS: "%s".\n\n' "$_OS" >&2
+ usage >&2
+ exit 2
+ fi
+ printf '%s' "$_PORT"
+}
+
+case "$ACTION" in
+ status)
+ echo "$VMS" | awk -F: '$0=$1' | while read -r vm; do
+ if [ -e "$PIDS_DIR/$vm.pid" ]; then
+ STATUS=up
+ else
+ STATUS=down
+ fi
+ printf '%s:%s\n' "$vm" "$STATUS"
+ done
+ ;;
+ up)
+ eval "$(assert-arg "$OS" 'OS')"
+ PORT="$(port_for_os "$OS")"
+
+ if [ -e "$PID_F" ]; then
+ printf 'The VM for "%s" is already running with PID %s.\n' \
+ "$OS" "$(cat "$PID_F")" >&2
+ else
+ QCOW="$XDG_STATE_HOME"/euandreh/qemu/"$OS".qcow2
+ qemu-system-x86_64 \
+ -m 1G \
+ -nic user,model=virtio,hostfwd=tcp::"$PORT"-:22 \
+ -drive file="$QCOW",media=disk,if=virtio \
+ -enable-kvm \
+ -nographic 1>/dev/null 2>&1 &
+ printf '%s' $! > "$PID_F"
+ fi
+ ;;
+ down)
+ eval "$(assert-arg "$OS" 'OS')"
+ PORT="$(port_for_os "$OS")"
+
+ if [ ! -e "$PID_F" ]; then
+ printf 'The VM for "%s" is not running, already.\n' "$OS" >&2
+ else
+ # shellcheck disable=2064
+ trap "rm -f $PID_F" EXIT
+ kill "$(cat "$PID_F")"
+ fi
+ ;;
+ *)
+ printf 'Unrecognized action: "%s".\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+esac
diff --git a/bin/volume b/bin/volume
new file mode 100755
index 0000000..991fbc7
--- /dev/null
+++ b/bin/volume
@@ -0,0 +1,112 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ volume ACTION
+ volume -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - up
+ - down
+ - toggle
+ - rotate
+
+
+ Manage the audio output.
+
+
+ Examples:
+
+ Increase the volume:
+
+ $ volume up
+
+
+ Toggle mute/unmute in the current audio output:
+
+ $ volume toggle
+
+
+ Change the audio output:
+
+ $ volume rotate
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+ACTION="${1:-}"
+
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+
+rotate() {
+ # This script assumes that at most 2 sinks exist at any time.
+ # When this premise is no longer true, it needs to be upgraded.
+
+ CURRENT="$(pacmd list-sinks | grep '\* index' | cut -d: -f2 | tr -d ' ')"
+ OTHER="$(pacmd list-sinks | grep index | grep -v '\* index' | tail -n1 | cut -d: -f2 | tr -d ' ')"
+
+ if [ "$CURRENT" = 0 ]; then
+ pacmd set-default-sink "$OTHER"
+ else
+ pacmd set-default-sink 0
+ fi
+}
+
+case "$ACTION" in
+ up)
+ pactl set-sink-volume @DEFAULT_SINK@ +10%
+ ;;
+ down)
+ pactl set-sink-volume @DEFAULT_SINK@ -10%
+ ;;
+ toggle)
+ pactl set-sink-mute @DEFAULT_SINK@ toggle
+ ;;
+ rotate)
+ rotate
+ ;;
+ *)
+ printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/with-email b/bin/with-email
new file mode 100755
index 0000000..7df101a
--- /dev/null
+++ b/bin/with-email
@@ -0,0 +1,91 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ with-email [-s SUBJECT] COMMAND...
+ with-email -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -s SUBJECT set the subject of the email
+ -h, --help show this message
+
+ COMMAND the command to be wrapped
+
+
+ Executes COMMAND and send all of its output via email
+ to eu@euandre.org.
+
+
+ Examples:
+
+ Run a script and use the default subject:
+
+ $ with-email -- ./script
+
+ Run a command and use a custom subject:
+
+ $ with-email -s 'CRONJOB' echo 123
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+SUBJECT='NO SUBJECT'
+while getopts 's:h' flag; do
+ case "$flag" in
+ s)
+ SUBJECT="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+eval "$(assert-arg "${1:-}" 'COMMAND...')"
+
+now() {
+ date '+%Y-%m-%dT%H:%M:%S%Z'
+}
+
+OUT="$(mkstemp)"
+{
+ printf 'Running command: %s\n' "$*"
+ printf 'Starting at: %s\n' "$(now)"
+ printf '\n'
+
+ STATUS=0
+ "$@" || STATUS=$?
+
+ printf '\n'
+ printf '\nFinished at: %s\n' "$(now)"
+} 1>"$OUT" 2>&1
+
+email -s "(exit status: $STATUS) - $SUBJECT" eu@euandre.org < "$OUT"
diff --git a/bin/without-env b/bin/without-env
new file mode 100755
index 0000000..fd9d1e8
--- /dev/null
+++ b/bin/without-env
@@ -0,0 +1,70 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ without-env ENVVAR PATH -- COMMAND...
+ without-env -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ Examples:
+
+ Execute "command -V" filtering ~/bin, to get where "w3m" is
+ in $PATH, other than ~/bin:
+ $ without-env PATH ~/bin -- command -v w3m
+
+ Compile foo.c, excluding ~/.local/include
+ from $C_INCLUDE_PATH:
+ $ without-env C_INCLUDE_PATH ~/.local/include -- cc -co foo.o foo.c
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+eval "$(assert-arg "${1:-}" 'ENVVAR')"
+eval "$(assert-arg "${2:-}" 'PATH')"
+
+eval "export $1=\"\$(echo \"\$$1\" | sed \"s|\$2:||g\")\""
+shift # drop $1
+shift # drop $2
+if [ "${1:-}" = '--' ]; then
+ shift # drop --
+fi
+
+"$@"
diff --git a/bin/wms b/bin/wms
new file mode 100755
index 0000000..d0a4d7c
--- /dev/null
+++ b/bin/wms
@@ -0,0 +1,95 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ wms ACTION
+ wms -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ ACTION one of:
+ - uuid
+ - date
+ - clear-notification
+
+
+ Helper script to launch CLI commands, without having complex
+ quoting, piping, flow control, etc. clutter the wm configuration
+ file.
+
+
+ Examples:
+
+ Generate a new UUID, copy it to the clipboard and send a
+ desktop notification:
+
+ $ wms uuid
+ EOF
+}
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+ACTION="${1:-}"
+
+eval "$(assert-arg "$ACTION" 'ACTION')"
+
+
+copy_and_notify() {
+ STR="$1"
+ LABEL="$2"
+ printf '%s' "$STR" | copy -n
+ notify-send -t 5000 -u normal -- \
+ "$STR" "$LABEL copied to clipboard"
+}
+
+case "$ACTION" in
+ uuid)
+ copy_and_notify "$(uuid)" 'UUID'
+ ;;
+ date)
+ copy_and_notify "$(date '+%Y-%m-%d')" 'date'
+ ;;
+ clear-notification)
+ dunstctl close
+ ;;
+ *)
+ printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2
+ usage >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/x b/bin/x
new file mode 100755
index 0000000..42f5770
--- /dev/null
+++ b/bin/x
@@ -0,0 +1,124 @@
+#!/usr/bin/env perl
+
+use v5.34;
+use warnings;
+use feature 'signatures';
+no warnings ('experimental::signatures', 'experimental::smartmatch');
+use Getopt::Std ();
+
+sub usage($fh) {
+ print $fh <<~'EOF'
+ Usage:
+ x COMMANDS... [ '&&' / '||' / '|' ] COMMANDS...
+ x COMMANDS... [ 'AND' / 'OR' / 'PIPE' ] COMMANDS...
+ x -h
+ EOF
+}
+
+sub help($fh) {
+ print $fh <<~'EOF'
+
+ Options:
+ -h, --help show this message
+
+ COMMAND the command to be executed
+ '&&' / AND "AND" logical operator
+ '||' / OR "OR" logical operator
+ '|' / PIPE pipe operator
+
+
+ Run command chained together with operators.
+
+ NOTE: Remember to quote '&&', 'OR' and '|' operators, otherwise
+ they'll get captured by the shell and not be passed to the 'x'
+ program!
+
+
+ Examples:
+
+ Measure the time of two commands:
+
+ $ time x sleep 1 '&&' sleep 2
+ # equivalent to:
+ $ time sh -c 'sleep 1 && sleep 2'
+
+
+ Notify when either of the commands finish:
+
+ $ boop x cmd-1 '||' cmd-2
+ EOF
+}
+
+
+for (@ARGV) {
+ last if $_ eq '--';
+ if ($_ eq '--help') {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+ }
+}
+
+my %opts;
+if (!Getopt::Std::getopts('h', \%opts)) {
+ usage *STDERR;
+ exit 2;
+}
+
+if ($opts{h}) {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+}
+
+
+sub status_for($n) {
+ if ($n == -1) {
+ return 127;
+ } elsif ($n & 127) {
+ return $n & 127;
+ } else {
+ return $n >> 8;
+ }
+}
+
+my @AND = ('&&', 'AND');
+my @OR = ('||', 'OR');
+my @PIPE = ('|', 'PIPE');
+my @OPS = (@AND, @OR, @PIPE);
+
+my @CMD;
+for (@ARGV) {
+ if ($_ ~~ @OPS) {
+ system @CMD;
+ @CMD = ();
+ if ($_ ~~ @AND && $?) {
+ exit status_for($?);
+ } elsif ($_ ~~ @OR && !$?) {
+ exit 0;
+ } elsif ($_ ~~ @PIPE) {
+ ...
+ }
+ } else {
+ push @CMD, $_;
+ }
+}
+
+exit status_for(system @CMD);
+
+
+__END__
+
+=head1 NAME
+
+x - chain shell commands without creating a subshell
+
+=head1 SYNOPSYS
+
+x COMMANDS... [ '&&' / '||' / '|' ] COMMANDS...
+
+x COMMANDS... [ 'AND' / 'OR' / 'PIPE' ] COMMANDS...
+
+x -h
+
+=cut
diff --git a/bin/xdg-open b/bin/xdg-open
new file mode 120000
index 0000000..ce4a72b
--- /dev/null
+++ b/bin/xdg-open
@@ -0,0 +1 @@
+open \ No newline at end of file
diff --git a/bin/xmpp b/bin/xmpp
new file mode 100755
index 0000000..b08a783
--- /dev/null
+++ b/bin/xmpp
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import getopt
+import logging
+import slixmpp
+
+USAGE = """\
+Usage:
+ xmpp [-d] [-F FROM_JID] -m MESSAGE TO_JID...
+ xmpp -h"""
+
+HELP = """
+Options:
+ -d run in DEBUG mode
+ -m MESSAGE the text of the message to be sent
+ -h, --help show this message
+
+ FROM_JID the address used to send the message from
+ TO_JID the addresses where to send the message to
+
+
+Send a one-off XMPP message.
+
+
+Examples:
+
+ Send a message to eu@euandreh.xyz:
+
+ $ xmpp -m 'Hello, XMPP!' eu@euandreh.xyz"""
+
+class SendMsgBot(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, on_start):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.on_start = on_start
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ self.on_start(self)
+ self.disconnect(wait=True)
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+ from_ = "bot@euandreh.xyz"
+ message = ""
+
+ for s in sys.argv:
+ if s == "--":
+ break
+ elif s == "--help":
+ print(USAGE)
+ print(HELP)
+ sys.exit()
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'm:F:dh')
+ except getopt.GetoptError as err:
+ print(err, file=sys.stderr)
+ print(USAGE, file=sys.stderr)
+ sys.exit(2)
+ for o, a in opts:
+ if o == "-m":
+ message = a
+ elif o == "-F":
+ from_ = a
+ elif o == "-d":
+ logging.basicConfig(level=logging.DEBUG)
+ elif o == "-h":
+ print(USAGE)
+ print(HELP)
+ sys.exit()
+ else:
+ assert False, "unhandled option"
+
+ if message == "":
+ print("Missing -m MESSAGE", file=sys.stderr)
+ print(USAGE, file=sys.stderr)
+ sys.exit(2)
+
+ if args == []:
+ print("Missing TO_JID", file=sys.stderr)
+ print(USAGE, file=sys.stderr)
+ sys.exit(2)
+
+ passcmd = "pass show VPS/kuvira/XMPP/" + from_ + " | head -n1 | tr -d '\\n'"
+ password = os.popen(passcmd).read()
+
+ def on_start(self):
+ for to in args:
+ self.send_message(mto=to, mbody=message, mtype='chat')
+
+ xmpp = SendMsgBot(from_, password, on_start)
+ xmpp.connect()
+ xmpp.process(forever=False)
+
+if __name__ == "__main__":
+ main()
diff --git a/bin/yt b/bin/yt
new file mode 100755
index 0000000..9aed64f
--- /dev/null
+++ b/bin/yt
@@ -0,0 +1,113 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ yt [-f] [-n PLAYLIST_COUNT] [URL...|FILE...|-]
+ yt -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -n PLAYLIST_COUNT the number of videos to grab from a
+ playlist (default: 15)
+ -f force to download a video already in the
+ archive
+ -h, --help show this message
+
+ URL an 'https://...' address
+ FILE a file with 'https://...' addresses, one
+ per line
+
+ Download videos and store them locally.
+
+
+ Examples:
+
+ Download the video from the given URL:
+
+ $ yt https://www.youtube.com/watch?v=EihZv2XgJdU
+
+
+ Force re-download the latest 5 videos already downloaded:
+
+ $ yt -nf5 https://www.youtube.com/watch?list=TLPQMTIwODIwMjLnL2XjyRRgSw
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+PLAYLIST_COUNT=15
+while getopts 'fn:h' flag; do
+ case "$flag" in
+ f)
+ FORCE=1
+ ;;
+ n)
+ PLAYLIST_COUNT="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+set -x
+
+if [ -z "${1:-}" ]; then
+ echo 'Missing URL|FILE argument' >&2
+ usage >&2
+ exit 2
+fi
+
+
+if [ ! -e "$1" ]; then
+ F="$(mkstemp)"
+ printf '%s\n' "$1" > "$F"
+else
+ F="$1"
+fi
+
+
+YT_TEMPLATE="$HOME/Downloads/yt-dl/%(uploader)s/%(upload_date)s %(title)s.%(ext)s"
+
+EXTRA_OPTIONS=''
+if [ -z "${FORCE:-}" ]; then
+ EXTRA_OPTIONS="--download-archive $HOME/Documents/yt-dl-seen.txt"
+fi
+
+# The value of $EXTRA_OPTIONS doesn't depend on user input, and can't contain
+# spaces, unless $HOME contains spaces:
+# shellcheck disable=2086
+youtube-dl \
+ --batch-file "$F" \
+ --format best \
+ --prefer-free-formats \
+ --playlist-end "$PLAYLIST_COUNT" \
+ --write-description \
+ --output "$YT_TEMPLATE" \
+ $EXTRA_OPTIONS
diff --git a/bin/z b/bin/z
new file mode 100755
index 0000000..0558436
--- /dev/null
+++ b/bin/z
@@ -0,0 +1,153 @@
+#!/usr/bin/env perl
+
+use v5.34;
+use warnings;
+use feature 'signatures';
+no warnings qw(experimental::signatures);
+use Getopt::Std ();
+use File::Temp ();
+use File::Basename ();
+use List::Util qw(any);
+
+
+sub usage($fh) {
+ print $fh <<~'EOF'
+ Usage:
+ z COMMANDS...
+ z -h
+ EOF
+}
+
+sub help($fh) {
+ print $fh <<~'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Wrapper that uncompresses file arguments to commands.
+ This enables having commands that operate on plain files to not
+ need to know if they're compressed or not.
+
+ It doesn't depend on the file extension, but on what file(1) says
+ of it.
+
+
+ Examples:
+
+ Replacement for zcat(1p):
+
+ $ z cat a-file.gz
+
+
+ Transparent grep (where my-file.dat is xz compressed):
+
+ $ z grep -E '[a-z]+' my-file.dat
+ EOF
+}
+
+for (@ARGV) {
+ last if $_ eq '--';
+ if ($_ eq '--help') {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+ }
+}
+
+my %opts;
+if (!Getopt::Std::getopts('h', \%opts)) {
+ usage *STDERR;
+ exit 2;
+}
+
+if ($opts{h}) {
+ usage *STDOUT;
+ help *STDOUT;
+ exit;
+}
+
+
+# Transform
+# FILENAME: content/type; charset=some\n
+# into
+# content/type
+sub trim($x) {
+ chomp $x;
+ $x =~ s/^.*?: //;
+ $x =~ s/;.*$//;
+ return $x;
+}
+
+my %TYPES = (
+ 'application/gzip' => [qw(gzip -dc)],
+ 'application/x-bzip2' => [qw(bzip2 -dc)],
+ 'application/x-xz' => [qw(xz -dc)],
+ 'application/x-lzma' => [qw(lzma -dc)],
+);
+
+my @tmpfiles;
+sub arg_for($arg) {
+ if (! -e $arg) {
+ return $arg;
+ }
+
+ my $type = trim `file -i $_`;
+ if (any { $type eq $_ } keys %TYPES) {
+ my $template = File::Basename::basename $arg . '.XXXXXX';
+ my ($fh, $tmpname) = File::Temp::tempfile(TEMPLATE => $template);
+ push @tmpfiles, $tmpname;
+ my @command = @{$TYPES{$type}};
+ print $fh `@command $arg`;
+ die $! if $?;
+ close $fh;
+ return $tmpname;
+ }
+
+ return $arg;
+}
+
+sub status_for($n) {
+ if ($n == -1) {
+ return 127;
+ } elsif ($n & 127) {
+ return $n & 127;
+ } else {
+ return $n >> 8;
+ }
+}
+
+my @CMD = map { arg_for $_ } @ARGV;
+exit status_for(system @CMD);
+
+END {
+ unlink @tmpfiles;
+}
+
+
+__END__
+
+=head1 NAME
+
+z - Wrapper that uncompresses file arguments to commands.
+
+=head1 SYNOPSIS
+
+z COMMAND FILE...
+
+=head1 DESCRIPTION
+
+Prefixing a shell command with "zrun" causes any compressed files that are
+arguments of the command to be transparently uncompressed to temp files
+(not pipes) and the uncompressed files fed to the command.
+
+This is a quick way to run a command that does not itself support
+compressed files, without manually uncompressing the files.
+
+The following compression types are supported: gz bz2 Z xz lzma lzo
+
+If zrun is linked to some name beginning with z, like zprog, and the link is
+executed, this is equivalent to executing "zrun prog".
+
+=cut