aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile142
-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
-rw-r--r--etc/afew/config6
-rw-r--r--etc/bash/inputrc2
-rw-r--r--etc/git/config25
-rw-r--r--etc/git/ignore3
-rw-r--r--etc/gnupg/gpg-agent.conf.tmpl6
-rw-r--r--etc/gnupg/gpg.conf1
-rw-r--r--etc/gnupg/sshcontrol1
-rw-r--r--etc/guile/init.scm6
-rw-r--r--etc/guix/channels.scm20
-rw-r--r--etc/guix/home.scm625
-rw-r--r--etc/guix/system.scm209
-rw-r--r--etc/hg/hgrc2
-rw-r--r--etc/i3/config185
-rw-r--r--etc/i3status/config31
-rw-r--r--etc/info/infokey7
-rw-r--r--etc/khal/config16
-rw-r--r--etc/khard/khard.conf13
-rw-r--r--etc/lisp-cli/init.lisp33
-rw-r--r--etc/mailcaps/config1
-rw-r--r--etc/newsboat/config1
l---------etc/newsboat/urls1
-rw-r--r--etc/python/pythonrc.py15
-rw-r--r--etc/ranger/rc.conf4
-rw-r--r--etc/remhind/config.tmpl4
-rwxr-xr-xetc/sh/cronjob.sh74
l---------etc/sh/privrc.sh1
-rw-r--r--etc/sh/rc364
-rw-r--r--etc/sh/root-rc87
-rw-r--r--etc/ssh/config.tmpl6
-rw-r--r--etc/ssh/known_hosts84
-rw-r--r--etc/tmux/tmux.conf96
l---------etc/weechat/irc.conf1
-rwxr-xr-xopt/aux/gen-e-list.sh24
l---------opt/bin-dirs/clisp1
l---------opt/bin-dirs/euandre.org1
-rwxr-xr-xopt/tests/assert-gpg-expiration.sh22
-rw-r--r--share/msg/bad.oggbin0 -> 12217 bytes
-rw-r--r--share/msg/good.oggbin0 -> 99826 bytes
109 files changed, 10601 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9907b46
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,142 @@
+.POSIX:
+
+
+pod2man = \
+ share/man/man1/x.1 \
+ share/man/man1/z.1 \
+
+
+lisp-images = \
+ $(XDG_DATA_HOME)/lisp-cli/clozure.image \
+ $(XDG_DATA_HOME)/lisp-cli/clisp.image \
+ $(XDG_DATA_HOME)/lisp-cli/sbcl.image \
+
+derived-assets = \
+ $(pod2man) \
+ $(XDG_CONFIG_HOME)/ssh/id_rsa.pub \
+ $(XDG_CONFIG_HOME)/git/config-extra \
+ $(XDG_CONFIG_HOME)/gnupg/gpg-agent.conf \
+ $(XDG_CONFIG_HOME)/remhind/config \
+ $(XDG_CONFIG_HOME)/ssh/config \
+ $(XDG_CONFIG_HOME)/alot/config \
+ $(XDG_CONFIG_HOME)/mbsync/config \
+ $(XDG_CONFIG_HOME)/msmtp/config \
+ $(XDG_CONFIG_HOME)/notmuch/default/config \
+ $(XDG_CONFIG_HOME)/notmuch/default/hooks/post-new \
+ $(XDG_DATA_HOME)/euandreh/mailcfg-accounts.txt \
+ $(XDG_DATA_HOME)/common-lisp/source \
+ $(XDG_DATA_HOME)/euandreh/e.list.txt \
+ $(lisp-images)
+
+
+all: $(derived-assets)
+
+
+share/man/man1/x.1: bin/x
+ pod2man bin/x > $@
+
+share/man/man1/z.1: bin/z
+ pod2man bin/z > $@
+
+$(XDG_DATA_HOME)/common-lisp/source:
+ ln -s $(SRC)/libre $@
+
+$(XDG_CONFIG_HOME)/alot/config: bin/mailcfg
+ mailcfg alot > $@
+
+$(XDG_CONFIG_HOME)/mbsync/config: bin/mailcfg
+ mailcfg mbsync > $@
+
+$(XDG_CONFIG_HOME)/msmtp/config: bin/mailcfg
+ mailcfg msmtp > $@
+
+$(XDG_CONFIG_HOME)/notmuch/default/config: bin/mailcfg
+ mailcfg notmuchcfg > $@
+
+$(XDG_CONFIG_HOME)/notmuch/default/hooks/post-new: bin/mailcfg
+ mailcfg notmuchhook > $@
+ chmod +x $@
+
+$(XDG_DATA_HOME)/euandreh/mailcfg-accounts.txt: bin/mailcfg
+ mailcfg list > $@
+
+$(XDG_CONFIG_HOME)/ssh/id_rsa.pub:
+ gpg --export-ssh-key eu@euandre.org > $@
+ chmod 600 $@
+
+$(XDG_CONFIG_HOME)/remhind/config: $(XDG_CONFIG_HOME)/remhind/config.tmpl
+ envsubst < $(XDG_CONFIG_HOME)/remhind/config.tmpl > $@
+
+$(XDG_CONFIG_HOME)/ssh/config: $(XDG_CONFIG_HOME)/ssh/config.tmpl
+ envsubst < $(XDG_CONFIG_HOME)/ssh/config.tmpl > $@
+
+$(XDG_CONFIG_HOME)/git/config-extra:
+ printf '[sendemail]\n smtpserver = ' > $@
+ command -v msmtpq >> $@
+
+$(XDG_CONFIG_HOME)/gnupg/gpg-agent.conf: $(XDG_CONFIG_HOME)/gnupg/gpg-agent.conf.tmpl
+ cp $(XDG_CONFIG_HOME)/gnupg/gpg-agent.conf.tmpl $@
+ printf 'pinentry-program ' >> $@
+ command -v pinentry-gtk-2 >> $@
+
+$(XDG_DATA_HOME)/euandreh/e.list.txt: ~/Documents/txt/ opt/aux/gen-e-list.sh
+ sh opt/aux/gen-e-list.sh > $@
+
+$(lisp-images): $(XDG_CONFIG_HOME)/lisp-cli/init.lisp bin/cl bin/li
+ I=`echo $@ | awk -F/ '$$0=$$(NF)' | cut -d. -f1` && \
+ li -vI $$I -E "(format t \"Image for \\\"$$I\\\" created.~%\")"
+
+
+
+check-shellcheck:
+ git ls-files | \
+ sor 'test -f' | \
+ xargs awk '/^#!\/bin\/sh$$/ { print FILENAME } { nextfile }' | \
+ xargs shellcheck -x
+
+check-perlcritic:
+ git ls-files | \
+ sor 'test -f' | \
+ xargs awk '/^#!\/usr\/bin\/env perl$$/ { print FILENAME } { nextfile }' | \
+ xargs perlcritic --exclude=subroutine
+
+FIXME-excludes = \
+ ':(exclude)Makefile' \
+ ':(exclude)etc/git/ignore' \
+ ':(exclude)opt/aux/gen-e-list.sh'
+check-fixme:
+ if git grep FIXME -- $(FIXME-excludes); then \
+ printf 'Leftover FIXME markers.\n' >&2; \
+ exit 1; \
+ fi
+
+check-dirty-public:
+ if ! git diff --quiet || ! git diff --quiet --staged; then \
+ printf 'Dirty tilde repository.\n' >&2; \
+ exit 1; \
+ fi
+
+check-dirty-private:
+ if ! git -C $(PRIV_CONFIG) diff --quiet || \
+ ! git -C $(PRIV_CONFIG) diff --quiet --staged; then \
+ printf 'Dirty private tilde repository.\n' >&2; \
+ exit 1; \
+ fi
+
+check-opt:
+ find opt/tests/ -name '*.sh' -exec {} +
+
+check-pod:
+ podchecker bin/z
+
+check-sync:
+ if git status --short --branch --porcelain | head -n1 | grep -E '(ahead|behind)'; then \
+ printf 'Out of sync with origin.\n' >&2; \
+ exit 1; \
+ fi
+
+check: check-shellcheck check-perlcritic check-fixme check-dirty-public \
+ check-dirty-private check-opt check-pod check-sync
+
+clean:
+ rm -f $(derived-assets)
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
diff --git a/etc/afew/config b/etc/afew/config
new file mode 100644
index 0000000..94341e9
--- /dev/null
+++ b/etc/afew/config
@@ -0,0 +1,6 @@
+[SpamFilter]
+[KillThreadsFilter]
+[ListMailsFilter]
+[ArchiveSentMailsFilter]
+[MeFilter]
+[InboxFilter]
diff --git a/etc/bash/inputrc b/etc/bash/inputrc
new file mode 100644
index 0000000..f5c7095
--- /dev/null
+++ b/etc/bash/inputrc
@@ -0,0 +1,2 @@
+"\e[B": history-search-forward
+"\e[A": history-search-backward
diff --git a/etc/git/config b/etc/git/config
new file mode 100644
index 0000000..0ff1257
--- /dev/null
+++ b/etc/git/config
@@ -0,0 +1,25 @@
+[user]
+ email = eu@euandre.org
+ name = EuAndreh
+ signingkey = 81F90EC3CD356060
+[transfer]
+ fsckobjects = true
+[push]
+ default = current
+[commit]
+ gpgsign = true
+ verbose = true
+[init]
+ defaultBranch = main
+[sendemail]
+ assume8bitEncoding = UTF-8
+ smtpserveroption = -a
+ smtpserveroption = EuAndreh
+ annotate = yes
+ confirm = never
+[remote "origin"]
+ fetch = +refs/notes/*:refs/notes/*
+[format]
+ useAutoBase = whenAble
+[include]
+ path = config-extra
diff --git a/etc/git/ignore b/etc/git/ignore
new file mode 100644
index 0000000..9103c3d
--- /dev/null
+++ b/etc/git/ignore
@@ -0,0 +1,3 @@
+/tmp/
+/FIXME
+/vendor/
diff --git a/etc/gnupg/gpg-agent.conf.tmpl b/etc/gnupg/gpg-agent.conf.tmpl
new file mode 100644
index 0000000..e772820
--- /dev/null
+++ b/etc/gnupg/gpg-agent.conf.tmpl
@@ -0,0 +1,6 @@
+# 2592000 = 60 * 60 * 24 * 30
+default-cache-ttl 172800
+default-cache-ttl-ssh 172800
+max-cache-ttl 2592000
+max-cache-ttl-ssh 2592000
+enable-ssh-support
diff --git a/etc/gnupg/gpg.conf b/etc/gnupg/gpg.conf
new file mode 100644
index 0000000..d4498fe
--- /dev/null
+++ b/etc/gnupg/gpg.conf
@@ -0,0 +1 @@
+keyserver pool.sks-keyservers.net
diff --git a/etc/gnupg/sshcontrol b/etc/gnupg/sshcontrol
new file mode 100644
index 0000000..1ae03ed
--- /dev/null
+++ b/etc/gnupg/sshcontrol
@@ -0,0 +1 @@
+750154E135FD7B11FDDF0107CC0904F92EBD2AE4
diff --git a/etc/guile/init.scm b/etc/guile/init.scm
new file mode 100644
index 0000000..9e962e8
--- /dev/null
+++ b/etc/guile/init.scm
@@ -0,0 +1,6 @@
+(use-modules
+ (ice-9 colorized)
+ (ice-9 readline))
+
+(activate-colorized)
+(activate-readline)
diff --git a/etc/guix/channels.scm b/etc/guix/channels.scm
new file mode 100644
index 0000000..f2da084
--- /dev/null
+++ b/etc/guix/channels.scm
@@ -0,0 +1,20 @@
+(append
+ (list
+ (channel
+ (name 'xyz-euandreh)
+ (url "git://euandreh.xyz/package-repository")
+ (branch "main")
+ (introduction
+ (make-channel-introduction
+ "d749e053e6db365069cb9b2ef47a78b06f9e7361"
+ (openpgp-fingerprint
+ "5BDA E9B8 B2F6 C6BC BB0D 6CE5 81F9 0EC3 CD35 6060"))))
+ (channel
+ (name 'nonguix)
+ (url "https://gitlab.com/nonguix/nonguix")
+ (introduction
+ (make-channel-introduction
+ "897c1a470da759236cc11798f4e0a5f7d4d59fbc"
+ (openpgp-fingerprint
+ "2A39 3FFF 68F4 EF7A 3D29 12AF 6F51 20A0 22FB B2D5")))))
+ %default-channels)
diff --git a/etc/guix/home.scm b/etc/guix/home.scm
new file mode 100644
index 0000000..fc9f2b5
--- /dev/null
+++ b/etc/guix/home.scm
@@ -0,0 +1,625 @@
+(use-modules
+ ((ice-9 textual-ports) #:prefix textual-ports:)
+ ((guix licenses) #:prefix licenses:)
+ ((xyz euandreh heredoc) #:prefix heredoc:)
+ (gnu home)
+ (gnu home services)
+ (gnu home services mcron)
+ (gnu home services shells)
+ (gnu home services shepherd)
+ (gnu home services xdg)
+ (gnu packages)
+ (gnu packages base)
+ (gnu packages dunst)
+ (gnu packages freedesktop)
+ (gnu packages gnupg)
+ (gnu packages gpodder)
+ (gnu packages libreoffice)
+ (gnu packages mail)
+ (gnu packages music)
+ (gnu packages wget)
+ (gnu packages lisp)
+ (gnu packages ssh)
+ (gnu packages texinfo)
+ (gnu packages tmux)
+ (gnu packages version-control)
+ (gnu packages video)
+ (gnu packages xdisorg)
+ (gnu services)
+ (guix build-system trivial)
+ (guix download)
+ (guix gexp)
+ (guix git-download)
+ (guix modules)
+ (guix packages)
+ (guix utils))
+(heredoc:enable-syntax)
+
+(define-public hunspell-iconv
+ (package
+ (inherit hunspell)
+ (name "hunspell-iconv")
+ (inputs
+ `(("libiconv" ,libiconv)
+ ,@(package-inputs hunspell)))))
+
+(define (hunspell-dictionary-utf8 dict-name)
+ (package
+ (name (string-append "hunspell-dict-" dict-name "-utf8"))
+ (version "630b34e6f8f3cbe7aa7b27b6d8ab118e27252fd1")
+ (source
+ (origin
+ (method git-fetch)
+ (uri
+ (git-reference
+ (url "https://github.com/wooorm/dictionaries")
+ (commit version)))
+ (file-name
+ (git-file-name "hunspell-dictionary-utf8" version))
+ (sha256
+ (base32 "1iknwzh5h04m067ddw9hlzc1qqj4mr9mdkcfapsnay304laaiyn5"))))
+ (build-system trivial-build-system)
+ (arguments
+ `(#:modules ((guix build utils))
+ #:builder
+ (begin
+ (use-modules (guix build utils))
+ (let ((source-prefix (string-append (assoc-ref %build-inputs "source")
+ "/dictionaries/"
+ ,dict-name
+ "/index"))
+ (install-prefix (string-append %output "/share/hunspell/")))
+ (mkdir-p install-prefix)
+ (copy-file (string-append source-prefix ".aff")
+ (string-append install-prefix ,dict-name ".aff"))
+ (copy-file (string-append source-prefix ".dic")
+ (string-append install-prefix ,dict-name ".dic"))))))
+ (synopsis (string-append "Hunspell " dict-name " dictionary in UTF-8"))
+ (description (string-append "Hunspell " dict-name " dictionary in UTF-8"))
+ (license licenses:expat)
+ (home-page "https://github.com/wooorm/dictionaries")))
+
+(define cmucl
+ (package
+ (name "cmucl-binary")
+ (version "21b")
+ (source
+ (origin
+ (method url-fetch)
+ (uri (string-append "https://common-lisp.net/project/cmucl/downloads/release/"
+ version
+ "/cmucl-"
+ version
+ "-x86-linux.tar.bz2"))
+ (sha256
+ (base32 "13k3b5ygnbsq6n2i3r5i4ljw3r1qlskn2p5f4x9hrx6vfvbb3k7a"))))
+ (build-system trivial-build-system)
+ (arguments
+ `(#:modules ((guix build utils))
+ #:builder
+ (begin
+ (use-modules (guix build utils))
+ (let ((source (assoc-ref %build-inputs "source"))
+ (bin (string-append %output "/bin")))
+ (mkdir-p bin)
+ (copy-file (string-append source "/bin/lisp")
+ (string-append bin "/lisp"))))))
+ (home-page "https://www.cons.org/cmucl/")
+ (synopsis "The CMU implementation of Common Lisp")
+ (description #"-
+ CMUCL is a free implementation of the Common Lisp programming language
+ which runs on most major Unix platforms. It mainly conforms to the
+ ANSI Common Lisp standard."#)
+ (license licenses:public-domain)))
+
+(define msmtp-non-hardcoded
+ (package
+ (inherit msmtp)
+ (name "msmtp-non-hardcoded")
+ (arguments
+ (substitute-keyword-arguments (package-arguments msmtp)
+ ((#:phases phases)
+ #~(modify-phases #$phases
+ (add-after 'install-additional-files 'patch-hardcoded-paths
+ (lambda* (#:key outputs #:allow-other-keys)
+ (let* ((out (assoc-ref outputs "out")))
+ (substitute* (string-append out "/bin/msmtpq")
+ (("LOG=.*$") "LOG=\"$XDG_LOG_HOME\"/msmtpq.log\n")
+ (("^Q=.*$") "Q=\"$XDG_DATA_HOME\"/msmtp/queue\n")
+ (("^MSMTPQ_Q=.*$") "MSMTPQ_Q=\"$Q\"\n")
+ (("mkdir -m 0700 \"\\$Q\"") "mkdir -p -m 0700 \"$Q\"")))))))))))
+
+(define gpodder-xdg
+ (package
+ (inherit gpodder)
+ (name "gpodder-xdg")
+ (arguments
+ (substitute-keyword-arguments (package-arguments gpodder)
+ ((#:phases phases '%standard-phases)
+ #~(modify-phases #$phases
+ (add-after 'install 'wrap-with-environment-variables
+ (lambda* (#:key outputs #:allow-other-keys)
+ (let ((out (assoc-ref outputs "out")))
+ (for-each
+ (lambda (bin)
+ (wrap-program (string-append out "/bin/" bin)
+ '("GPODDER_HOME" = ("${XDG_DATA_HOME:-$HOME/.local/share}/gPodder"))
+ '("GPODDER_DOWNLOAD_DIR" = ("${XDG_DOWNLOAD_DIR:-$HOME/Downloads}/gPodder"))))
+ '("gpo" "gpodder")))))))))))
+
+(define (with-options pkg bin opts)
+ (package
+ (inherit pkg)
+ (arguments
+ (substitute-keyword-arguments (package-arguments pkg)
+ ((#:phases phases '%standard-phases)
+ `(modify-phases ,phases
+ (add-after 'install 'wrap-with-flags
+ (lambda* (#:key outputs #:allow-other-keys)
+ (define (wrap-options prog options)
+ (let ((wrapped-file (string-append (dirname prog) "/." (basename prog) "-orig")))
+ (rename-file prog wrapped-file)
+ (call-with-output-file prog
+ (lambda (port)
+ (format port
+ "#!/bin/sh~%~%exec \"~a\" ~a \"$@\"~%"
+ (canonicalize-path wrapped-file)
+ options)))
+ (chmod prog #o755)))
+ (wrap-options (string-append (assoc-ref outputs "out")
+ "/bin/"
+ ,bin)
+ ,opts)))))))))
+
+(define isync-with-options
+ (with-options isync "mbsync" "--config=\"$XDG_CONFIG_HOME\"/mbsync/config"))
+
+(define wget-with-options
+ (with-options wget "wget" "--hsts-file=\"$XDG_STATE_HOME\"/wget-hsts"))
+
+(define tmux-with-options
+ (with-options tmux "tmux" "-f \"$XDG_CONFIG_HOME\"/tmux/tmux.conf"))
+
+(define texinfo-with-options
+ (with-options texinfo "info" "--init-file \"$XDG_CONFIG_HOME\"/info/infokey"))
+
+(define mpv-with-options
+ (with-options mpv "mpv" (string-append "--script="
+ (getenv "HOME")
+ "/.guix-home/profile/lib/mpris.so")))
+
+(define openssh-with-options
+ (with-options openssh "ssh" "-F \"$XDG_CONFIG_HOME\"/ssh/config"))
+
+(define (xdg-config-home s)
+ (string-append (getenv "XDG_CONFIG_HOME") "/" s))
+
+
+(define (slurp name)
+ (string-trim-both
+ (call-with-input-file
+ name
+ textual-ports:get-string-all)))
+
+(define (script name content)
+ (package
+ (name name)
+ (version "latest")
+ (source #f)
+ (build-system trivial-build-system)
+ (arguments
+ `(#:modules ((guix build utils))
+ #:builder
+ (begin
+ (use-modules (guix build utils))
+ (let* ((bin (string-append %output "/bin"))
+ (prog (string-append bin "/" ,name)))
+ (mkdir-p bin)
+ (call-with-output-file prog
+ (lambda (port)
+ (format port "~a" ,content)))
+ (chmod prog #o755)))))
+ (home-page "")
+ (synopsis "")
+ (description "")
+ (license #f)))
+
+(define cronjobs
+ (list
+ #~(job "0 0 * * *" "cronjob msmtp-queue -r")
+ #~(job "0 0 * * *" "cronjob check")
+ #~(job "5 */6 * * *" "cronjob m")
+ #~(job "30 0 * * *" "cronjob x update AND upgrade")
+ #~(job "30 0 * * *" "cronjob backup -q cron")))
+
+(define (home-service name package bin)
+ (service-type
+ (name name)
+ (extensions
+ (list
+ (service-extension home-shepherd-service-type
+ (lambda _
+ (list
+ (shepherd-service
+ (provision (list name))
+ (documentation
+ (format #f "Shepherd service that manages ~a." name))
+ (start
+ #~(make-forkexec-constructor
+ (list #$(file-append package bin))))
+ (stop #~(make-kill-destructor))))))
+ (service-extension home-profile-service-type
+ (lambda _ (list package)))))
+ (default-value '())
+ (description
+ (format #f
+ #"-
+ Service that runs ~a as a daemon under Shepherd.
+
+ It has no configuration."#
+ name))))
+
+(define xdg-prefix "$HOME/.usr")
+(define (xdg path)
+ (string-append xdg-prefix "/" path))
+
+(home-environment
+ (packages
+ (append
+ (map (compose list specification->package+output symbol->string)
+ '(nss-certs
+ bash
+ coreutils
+ findutils
+ diffutils
+ grep
+ sed
+ tar
+ gawk
+ bc
+ nvi
+ patchelf
+
+ man-pages
+ man-pages-posix
+
+ bash-completion
+
+ git
+ git:send-email
+ git:gui
+ git-open
+ git-remote-gcrypt
+ git-lfs
+ mercurial
+ fossil
+ darcs
+ subversion
+ cvs
+ rcs
+ cssc
+ quilt
+
+ gnupg
+ rsync
+ tree
+ diffoscope
+ mailutils
+ entr
+ pulseaudio
+ password-store
+ playerctl
+ pinentry-gtk2
+ bmake
+ make
+ tup
+ autoconf
+ automake
+ libtool
+ pcre:out
+ pcre:bin
+ pcre:doc
+ pcre:static
+ pcre2
+ avahi
+ libgcrypt
+ qbe
+ cproc
+ doxygen
+ gperf
+ readline
+ gmp
+ help2man
+ libtomcrypt
+ libtommath
+ lz4
+ lokke
+ meson
+ ninja
+ sparse
+ ant
+ mpc
+ maven
+ pkg-config
+ fzf
+ ranger
+ blueman
+ pavucontrol
+ ledger
+ bind:utils
+ stunnel
+ netcat
+ siege
+ curl
+ curl:doc
+ xclip
+ cloc
+ strace
+ file@5.39
+ urlscan
+ rlwrap
+ direnv
+ borg
+ khal
+ khard
+ libfaketime
+ qrencode
+ feh
+ sox
+ xset
+ graphviz
+ moreutils
+ shellcheck
+ gettext
+ lilypond
+ groff
+ groff:doc
+ grap
+ ghostscript
+ texlive
+ jq
+ recutils
+ units
+ ncurses
+ trash-cli
+ lsof
+ autojump
+ unzip
+ powertop
+ md4c
+ timidity++
+ cmark
+ cmake
+ makefile2graph
+ po4a
+ mdpo
+ universal-ctags
+ gron
+ reptyr
+ xpdf
+ perf-tools
+ scdoc
+ rpm
+ cpio
+
+ ;; for compiling ECL
+ libatomic-ops
+ libgc
+ libffi
+ ;; for compiling CLISP
+ libffcall
+ libsigsegv
+
+ cryptsetup
+ btrfs-progs
+
+ flatpak
+ xdg-desktop-portal
+
+ sqlite
+ clojure
+ clojure-tools
+ leiningen
+ openjdk
+ perl
+ perl-dbi
+ perl-dbd-sqlite
+ perl-critic
+ perl-json
+ perl-mojolicious
+ perl-regexp-grammars
+ perl-commonmark
+ perl-aliased
+ perl-uri-escape
+ ruby
+ python
+ python-sphinx
+ python-slixmpp
+ python-unidecode
+ python-yubikey-manager
+ python-coverage
+ python-pytest
+ python-requests
+ python-beautifulsoup4
+ python-docx
+ python-telegram-bot
+ ; python-docutils ; broken: conflicts with python-sphinx
+ python-mkdocs
+ valgrind
+ flex
+ bison
+ gcc-toolchain
+ clang
+ tcc
+ fuse
+ ; node ; broken: conflicts with archivebox
+ quickjs
+ m4
+ go
+ xrandr
+ arandr
+ openssl
+ fswatch
+ ;; rust ; broken
+ ;; rust:cargo ; broken
+ ;; rust:rustfmt ; broken
+ vala
+ tcl
+
+ sbcl
+ gcl
+ ecl
+ clisp
+ ccl
+ abcl
+ janet
+ kawa
+ chez-scheme
+ racket
+ chibi-scheme
+ ; chicken ; broken: conflicts with gcc-toolchain
+ gambit-c
+ gauche
+
+ dash
+ fish
+ rc
+ es
+ tcsh
+ zsh
+ oksh
+ loksh
+ mksh
+ oil
+ gash
+ nushell
+
+ gtk
+ gtk:bin
+ gtk:doc
+ glade
+ ; libglade ; broken: conflicts with zathura
+ cambalache
+ tk
+ qtbase
+ qtbase:debug
+ qtdeclarative
+
+ st
+ i3status
+ xmessage
+ dmenu
+ httpd ;; for htpasswd
+
+ weechat
+ alot
+ notmuch
+ w3m
+ afew
+ qtox
+ telescope
+ imagemagick
+ ffmpeg
+ pandoc
+ mktorrent
+ jekyll
+ flac
+ mediainfo
+ libnotify
+ espeak-ng
+ procps
+ htop
+ zenity
+ util-linux
+ lightning
+ lmdb
+ guile
+ guile-heredoc-latest
+ gzip
+ xz
+ bzip2
+ lzip
+ lzop
+ which
+ libxml2
+ psmisc
+ less
+ nano
+ patch
+ youtube-dl
+ tmux-plugin-resurrect
+ tmux-plugin-continuum
+
+ ;; ArchiveBox and some of its optional dependencies
+ archivebox
+ ripgrep
+
+ poezio
+ freetalk
+ mcabber
+ profanity
+ newsboat
+ mpv-mpris
+ vlc
+ hicolor-icon-theme
+
+ keepassxc
+
+ xbacklight
+
+ gnote
+ telegram-desktop
+ zathura
+ zathura-djvu
+ zathura-pdf-poppler
+ zathura-ps
+ dino
+ poedit
+ transmission
+ transmission:gui
+ audacity
+ inkscape
+ libreoffice
+ quodlibet
+ ungoogled-chromium
+ icedove
+ firefox))
+ (list msmtp-non-hardcoded
+ ;; cmucl
+ isync-with-options
+ wget-with-options
+ tmux-with-options
+ texinfo-with-options
+ mpv-with-options
+ openssh-with-options
+ hunspell-iconv
+ gpodder-xdg
+ (hunspell-dictionary-utf8 "en")
+ (hunspell-dictionary-utf8 "pt")
+ (hunspell-dictionary-utf8 "fr")
+ (hunspell-dictionary-utf8 "eo")
+ (script "cronjob" (slurp (string-append (getenv "XDG_CONFIG_HOME")
+ "/sh/cronjob.sh"))))))
+ (services
+ (list
+ (service (home-service 'clipmenu clipmenu "/bin/clipmenud"))
+ (service (home-service 'dunst dunst "/bin/dunst"))
+ (service (home-service 'poweralertd poweralertd "/bin/poweralertd"))
+ (service home-xdg-base-directories-service-type
+ (home-xdg-base-directories-configuration
+ (cache-home (xdg "var/cache"))
+ (config-home (xdg "etc"))
+ (data-home (xdg "share"))
+ (log-home (xdg "var/log"))
+ (state-home (xdg "var/state"))))
+ (simple-service 'my-shell-profile home-shell-profile-service-type
+ (list (plain-file
+ "my-profile"
+ (format #f
+ #"-
+ export XDG_PREFIX="~a"
+ . "$XDG_CONFIG_HOME"/sh/rc"#
+ xdg-prefix))))
+ (service home-mcron-service-type
+ (home-mcron-configuration
+ (jobs cronjobs))))))
diff --git a/etc/guix/system.scm b/etc/guix/system.scm
new file mode 100644
index 0000000..fddadca
--- /dev/null
+++ b/etc/guix/system.scm
@@ -0,0 +1,209 @@
+(use-modules
+ ((xyz euandreh heredoc) #:prefix heredoc:)
+ (gnu bootloader)
+ (gnu bootloader grub)
+ (gnu packages)
+ (gnu services base)
+ (gnu services cups)
+ (gnu services desktop)
+ (gnu services docker)
+ (gnu services pm)
+ (gnu services security-token)
+ (gnu services sound)
+ (gnu services ssh)
+ (gnu services virtualization)
+ (gnu services vpn)
+ (gnu services xorg)
+ (gnu system keyboard)
+ (gnu system file-systems)
+ (gnu system locale)
+ (gnu system mapped-devices)
+ (guix gexp)
+ (guix packages)
+ (nongnu packages linux)
+ (nongnu system linux-initrd)
+ (srfi srfi-1)
+ (xyz euandreh queue))
+(heredoc:enable-syntax)
+
+(operating-system
+ (kernel linux)
+ (initrd microcode-initrd)
+ (firmware (list linux-firmware))
+ (locale "fr_FR.UTF-8")
+ (locale-definitions
+ (append
+ (list
+ (locale-definition
+ (name "pt_BR.UTF-8")
+ (source "pt_BR")))
+ %default-locale-definitions))
+ (timezone "America/Sao_Paulo")
+ (keyboard-layout
+ (keyboard-layout "br" #:options '("caps:swapescape" "esperanto:qwerty")))
+ (host-name "velhinho")
+ (hosts-file
+ (plain-file
+ "hosts"
+ (format #f
+ #"-
+ 127.0.0.1 localhost ~a
+ ::1 localhost ~a
+
+ 10.0.2.1 kuvira.wg
+ 10.0.2.2 velhinho.wg
+ "#
+ host-name
+ host-name)))
+ (users
+ (append
+ (list
+ (user-account
+ (name "andreh")
+ (comment "EuAndreh")
+ (group "users")
+ (supplementary-groups '("netdev" "audio" "video" "wheel" "kvm" "docker"))))
+ %base-user-accounts))
+ (packages
+ (append
+ (map (compose list specification->package+output symbol->string)
+ '(nss-certs
+ i3-wm
+ guile
+ guile-heredoc-latest))
+ (list)
+ (remove (lambda (package)
+ (equal? "wget" (package-name package)))
+ %base-packages)))
+ (services
+ (append
+ (list
+ (service bluetooth-service-type)
+ (service tlp-service-type)
+ (service thermald-service-type)
+ (service pcscd-service-type)
+ (service docker-service-type)
+ (service libvirt-service-type)
+ (service virtlog-service-type)
+ (service xfce-desktop-service-type)
+ (service mate-desktop-service-type)
+ (service lxqt-desktop-service-type)
+ (service enlightenment-desktop-service-type)
+ (service gnome-desktop-service-type)
+ (service gnome-keyring-service-type)
+ (service wireguard-service-type
+ (wireguard-configuration
+ (addresses '("10.0.2.2/24"))
+ (peers
+ (list
+ (wireguard-peer
+ (name "kuvira")
+ (endpoint "euandreh.xyz:51820")
+ (public-key "FwXqY9wXO8jK/D7lyprI+cslVeb9AqOQBAbxKG6S5lE=")
+ (allowed-ips '("10.0.2.1/32"))
+ (keep-alive 25))))))
+ (service qemu-binfmt-service-type
+ (qemu-binfmt-configuration
+ (platforms
+ (lookup-qemu-platforms "arm" "aarch64"))))
+ (service cups-service-type
+ (cups-configuration
+ (web-interface? #t)
+ (extensions
+ (list epson-L365))))
+ (service openssh-service-type
+ (openssh-configuration
+ (password-authentication? #f)
+ (authorized-keys
+ `(("andreh" ,(local-file
+ (string-append (or (getenv "XDG_CONFIG_HOME")
+ (string-append (getenv "HOME") "/.ssh"))
+ "/ssh/id_rsa.pub")))))
+ (extra-content #"-
+ ClientAliveInterval 30
+ ClientAliveCountMax 20
+ MaxSessions 20
+ "#)))
+ #;
+ (udev-rules-service
+ 'backlight
+ (udev-rule
+ "backlight.rule"
+ (string-replace
+ #"-
+ ACTION=="add", SUBSYSTEM=="backlight", KERNEL=="@DEVICE@", GROUP="video", MODE="0664"
+ "#
+ "@DEVICE@"
+ (getenv "BACKLIGHT_DEVICE"))))
+ (set-xorg-configuration
+ (xorg-configuration
+ (keyboard-layout keyboard-layout)
+ (extra-config
+ (list
+ #"-
+ Section "InputClass"
+ Identifier "touchpad"
+ Driver "libinput"
+ MatchIsTouchpad "on"
+ Option "Tapping" "on"
+ EndSection
+ Section "Device"
+ Identifier "Intel Graphics"
+ Driver "intel"
+ Option "Backlight" "intel_backlight"
+ EndSection
+ "#)))))
+ (modify-services %desktop-services
+ (pulseaudio-service-type config =>
+ (pulseaudio-configuration
+ (inherit config)
+ (extra-script-files
+ (list
+ (plain-file
+ "noise-cancelling.pa"
+ #"-
+ load-module module-echo-cancel
+ "#)))))
+ (guix-service-type config =>
+ (guix-configuration
+ (inherit config)
+ (substitute-urls
+ (append
+ '("https://substitutes.nonguix.org")
+ %default-substitute-urls))
+ (authorized-keys
+ (append
+ (list
+ (plain-file
+ "non-guix.pub"
+ #"-
+ (public-key
+ (ecc
+ (curve Ed25519)
+ (q #C1FD53E5D4CE971933EC50C9F307AE2171A2D3B52C804642A7A35F84F3A4EA98#)))
+ "#))
+ %default-authorized-guix-keys)))))))
+ (bootloader
+ (bootloader-configuration
+ (bootloader grub-efi-bootloader)
+ (targets '("/boot/efi"))
+ (keyboard-layout keyboard-layout)))
+ (mapped-devices
+ (list
+ (mapped-device
+ (source (uuid "6b0d38a6-d93e-4f8e-a59a-7729f5adf892"))
+ (target "cryptroot")
+ (type luks-device-mapping))))
+ (file-systems
+ (append
+ (list
+ (file-system
+ (mount-point "/boot/efi")
+ (device (uuid "1B26-9F4E" 'fat32))
+ (type "vfat"))
+ (file-system
+ (mount-point "/")
+ (device "/dev/mapper/cryptroot")
+ (type "ext4")
+ (dependencies mapped-devices)))
+ %base-file-systems)))
diff --git a/etc/hg/hgrc b/etc/hg/hgrc
new file mode 100644
index 0000000..656f40b
--- /dev/null
+++ b/etc/hg/hgrc
@@ -0,0 +1,2 @@
+[ui]
+username = EuAndreh <eu@euandre.org>
diff --git a/etc/i3/config b/etc/i3/config
new file mode 100644
index 0000000..94648b2
--- /dev/null
+++ b/etc/i3/config
@@ -0,0 +1,185 @@
+set $mod Mod4
+
+
+# font pango:monospace 8
+
+# This font is widely installed, provides lots of unicode glyphs, right-to-left
+# text rendering and scalability on retina/hidpi displays (thanks to pango).
+font pango:DejaVu Sans Mono 8
+
+# Start XDG autostart .desktop files using dex. See also
+# https://wiki.archlinux.org/index.php/XDG_Autostart
+# exec --no-startup-id dex --autostart --environment i3
+
+# The combination of xss-lock, nm-applet and pactl is a popular choice, so
+# they are included here as an example. Modify as you see fit.
+
+# xss-lock grabs a logind suspend inhibit lock and will use i3lock to lock the
+# screen before suspend. Use loginctl lock-session to lock your screen.
+exec xss-lock --transfer-sleep-lock -- i3lock --nofork
+
+# NetworkManager is the most popular way to manage wireless networks on Linux,
+# and nm-applet is a desktop environment-independent system tray GUI for it.
+exec nm-applet
+exec blueman-applet
+
+# Use pactl to adjust volume in PulseAudio.
+set $refresh_i3status killall -SIGUSR1 i3status
+bindsym F1 exec volume toggle && $refresh_i3status
+bindsym F2 exec volume down && $refresh_i3status
+bindsym F3 exec volume up && $refresh_i3status
+bindsym $mod+Shift+s exec volume rotate && $refresh_i3status
+
+bindsym F4 exec player backward
+bindsym Shift+F4 exec player previous
+bindsym F5 exec player play-pause
+Bindsym Shift+F5 exec player rotate
+bindsym F6 exec player forward
+bindsym Shift+F6 exec player next
+
+bindsym F7 exec brightness -1
+bindsym F8 exec brightness +1
+
+
+
+
+
+# Use Mouse+$mod to drag floating windows to their wanted position
+floating_modifier $mod
+
+bindsym $mod+Return exec st
+bindsym $mod+Shift+q kill
+
+
+
+
+bindsym $mod+u exec wms uuid
+bindsym $mod+t exec wms date
+bindsym $mod+m exec wms clear-notification
+bindsym $mod+p exec menu bin
+bindsym $mod+o exec menu emoji
+bindsym $mod+v exec menu clipboard
+bindsym $mod+i exec menu password
+bindsym $mod+Control+i exec menu username
+bindsym $mod+Control+Shift+i exec menu yubikey
+
+
+
+
+# change focus
+bindsym $mod+h focus left
+bindsym $mod+j focus down
+bindsym $mod+k focus up
+bindsym $mod+l focus right
+
+# move focused window
+bindsym $mod+Shift+h move left
+bindsym $mod+Shift+j move down
+bindsym $mod+Shift+k move up
+bindsym $mod+Shift+l move right
+
+# bindsym $mod+h split h # what is this?
+
+# split in vertical orientation
+bindsym $mod+Shift+v split v
+
+# enter fullscreen mode for the focused container
+bindsym $mod+f fullscreen toggle
+
+# change container layout (stacked, tabbed, toggle split)
+bindsym $mod+s layout stacking
+bindsym $mod+w layout tabbed
+bindsym $mod+e layout toggle split
+
+# toggle tiling / floating
+bindsym $mod+Shift+space floating toggle
+
+# change focus between tiling / floating windows
+bindsym $mod+space focus mode_toggle
+
+# focus the parent container
+bindsym $mod+a focus parent
+
+# focus the child container
+bindsym $mod+d focus child
+
+# Define names for default workspaces for which we configure key bindings later on.
+# We use variables to avoid repeating the names in multiple places.
+set $ws1 "1"
+set $ws2 "2"
+set $ws3 "3"
+set $ws4 "4"
+set $ws5 "5"
+set $ws6 "6"
+set $ws7 "7"
+set $ws8 "8"
+set $ws9 "9"
+set $ws10 "10"
+
+# switch to workspace
+bindsym $mod+1 workspace number $ws1
+bindsym $mod+2 workspace number $ws2
+bindsym $mod+3 workspace number $ws3
+bindsym $mod+4 workspace number $ws4
+bindsym $mod+5 workspace number $ws5
+bindsym $mod+6 workspace number $ws6
+bindsym $mod+7 workspace number $ws7
+bindsym $mod+8 workspace number $ws8
+bindsym $mod+9 workspace number $ws9
+bindsym $mod+0 workspace number $ws10
+
+# move focused container to workspace
+bindsym $mod+Shift+1 move container to workspace number $ws1
+bindsym $mod+Shift+2 move container to workspace number $ws2
+bindsym $mod+Shift+3 move container to workspace number $ws3
+bindsym $mod+Shift+4 move container to workspace number $ws4
+bindsym $mod+Shift+5 move container to workspace number $ws5
+bindsym $mod+Shift+6 move container to workspace number $ws6
+bindsym $mod+Shift+7 move container to workspace number $ws7
+bindsym $mod+Shift+8 move container to workspace number $ws8
+bindsym $mod+Shift+9 move container to workspace number $ws9
+bindsym $mod+Shift+0 move container to workspace number $ws10
+
+# reload the configuration file
+bindsym $mod+Shift+c reload
+# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
+bindsym $mod+Shift+r restart
+# exit i3 (logs you out of your X session)
+bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -B 'Yes, exit i3' 'i3-msg exit'"
+
+# resize window (you can also use the mouse for that)
+mode "resize" {
+ # These bindings trigger as soon as you enter the resize mode
+
+ # Pressing left will shrink the window’s width.
+ # Pressing right will grow the window’s width.
+ # Pressing up will shrink the window’s height.
+ # Pressing down will grow the window’s height.
+ bindsym h resize shrink width 10 px or 10 ppt
+ bindsym j resize grow height 10 px or 10 ppt
+ bindsym k resize shrink height 10 px or 10 ppt
+ bindsym l resize grow width 10 px or 10 ppt
+
+ # same bindings, but for the arrow keys
+ # bindsym Left resize shrink width 10 px or 10 ppt
+ # bindsym Down resize grow height 10 px or 10 ppt
+ # bindsym Up resize shrink height 10 px or 10 ppt
+ # bindsym Right resize grow width 10 px or 10 ppt
+
+ # back to normal: Enter or Escape or $mod+r
+ bindsym Return mode "default"
+ bindsym Caps_Lock mode "default"
+ bindsym $mod+r mode "default"
+}
+
+bindsym $mod+r mode "resize"
+
+# Start i3bar to display a workspace bar (plus the system information i3status
+# finds out, if available)
+bar {
+ position top
+ status_command i3status | status-bar
+}
+
+default_border pixel
+default_floating_border pixel
diff --git a/etc/i3status/config b/etc/i3status/config
new file mode 100644
index 0000000..8099a2f
--- /dev/null
+++ b/etc/i3status/config
@@ -0,0 +1,31 @@
+general {
+ output_format = "i3bar"
+ colors = true
+}
+
+order += "wireless _first_"
+order += "battery all"
+order += "disk /"
+order += "memory"
+order += "tztime local"
+
+wireless _first_ {
+ format_up = "%essid ~%quality"
+}
+
+battery all {
+ format = "bat: %status %percentage %remaining"
+}
+
+disk "/" {
+ format = "disk: %avail"
+}
+
+memory {
+ format = "mem: %used/%total"
+ threshold_degraded = "1G"
+}
+
+tztime local {
+ format = "%A, %Y-%m-%d %H:%M:%S"
+}
diff --git a/etc/info/infokey b/etc/info/infokey
new file mode 100644
index 0000000..90edc50
--- /dev/null
+++ b/etc/info/infokey
@@ -0,0 +1,7 @@
+^e down-line
+^y up-line
+
+#var
+link-style=cyan
+active-link-style=blue,bold
+match-style=underline,bold,bggreen
diff --git a/etc/khal/config b/etc/khal/config
new file mode 100644
index 0000000..f42a87b
--- /dev/null
+++ b/etc/khal/config
@@ -0,0 +1,16 @@
+[calendars]
+
+[[private]]
+path = $XDG_DATA_HOME/khal/calendars/private/
+type = calendar
+
+[locale]
+timeformat = %H:%M
+dateformat = %d/%m/%Y
+longdateformat = %d/%m/%Y
+datetimeformat = %d/%m/%Y %H:%M
+longdatetimeformat = %d/%m/%Y %H:%M
+default_timezone = America/Sao_Paulo
+
+[default]
+default_calendar = private
diff --git a/etc/khard/khard.conf b/etc/khard/khard.conf
new file mode 100644
index 0000000..e6ef986
--- /dev/null
+++ b/etc/khard/khard.conf
@@ -0,0 +1,13 @@
+[addressbooks]
+
+[[private]]
+path = $XDG_DATA_HOME/khard/contacts/private/
+
+[general]
+default_action = list
+
+[contact table]
+show_nicknames = yes
+
+[vcard]
+preferred_version = 4.0
diff --git a/etc/lisp-cli/init.lisp b/etc/lisp-cli/init.lisp
new file mode 100644
index 0000000..69cb054
--- /dev/null
+++ b/etc/lisp-cli/init.lisp
@@ -0,0 +1,33 @@
+#-quicklisp
+(let ((quicklisp-init (merge-pathnames "dev/quicklisp/setup.lisp"
+ (user-homedir-pathname))))
+ (when (probe-file quicklisp-init)
+ (load quicklisp-init)))
+
+(setf ql:*quickload-verbose* t)
+
+(defun load-once (p)
+ (let ((k (intern
+ (concatenate 'string
+ (string :ql/)
+ (string p))
+ "KEYWORD")))
+ (unless (member k *features*)
+ (ql:quickload p :verbose t)
+ (pushnew k *features*))
+ k))
+
+(mapcar #'load-once
+ (list
+ :cl-ppcre
+ :cffi
+ :trivial-dump-core
+ :named-readtables
+ :rstring))
+
+(mapcar (lambda (p)
+ (pushnew (concatenate 'string p "/") cffi:*foreign-library-directories*
+ :test #'equal))
+ (cl-ppcre:split ":" (uiop:getenv "LIBRARY_PATH")))
+
+(load-once :cl-fswatch)
diff --git a/etc/mailcaps/config b/etc/mailcaps/config
new file mode 100644
index 0000000..60f7286
--- /dev/null
+++ b/etc/mailcaps/config
@@ -0,0 +1 @@
+text/html; env HOME=$XDG_DATA_HOME/w3m w3m -dump -o document_charset=%{charset} '%s'; nametemplate=%s.html; copiousoutput
diff --git a/etc/newsboat/config b/etc/newsboat/config
new file mode 100644
index 0000000..b138e1d
--- /dev/null
+++ b/etc/newsboat/config
@@ -0,0 +1 @@
+text-width 80
diff --git a/etc/newsboat/urls b/etc/newsboat/urls
new file mode 120000
index 0000000..c7c1b2d
--- /dev/null
+++ b/etc/newsboat/urls
@@ -0,0 +1 @@
+../../var/lib/private/tilde/newsboat/urls \ No newline at end of file
diff --git a/etc/python/pythonrc.py b/etc/python/pythonrc.py
new file mode 100644
index 0000000..1fc3bfe
--- /dev/null
+++ b/etc/python/pythonrc.py
@@ -0,0 +1,15 @@
+import os
+import atexit
+import readline
+
+history = os.path.join(os.environ["XDG_STATE_HOME"], "python-history")
+
+try:
+ readline.read_history_file(history)
+except OSError:
+ pass
+
+def write_history():
+ readline.write_history_file(history)
+
+atexit.register(write_history)
diff --git a/etc/ranger/rc.conf b/etc/ranger/rc.conf
new file mode 100644
index 0000000..2f1d9c9
--- /dev/null
+++ b/etc/ranger/rc.conf
@@ -0,0 +1,4 @@
+map DD shell trash %s
+map XX shell rm -rf %s
+map <C-e> scroll_preview 1
+map <C-y> scroll_preview -1
diff --git a/etc/remhind/config.tmpl b/etc/remhind/config.tmpl
new file mode 100644
index 0000000..4d74b20
--- /dev/null
+++ b/etc/remhind/config.tmpl
@@ -0,0 +1,4 @@
+[calendars]
+ [calendars.private_cal]
+ name = "private_cal"
+ path = "${XDG_DATA_HOME}/khal/calendars/private/"
diff --git a/etc/sh/cronjob.sh b/etc/sh/cronjob.sh
new file mode 100755
index 0000000..0569b44
--- /dev/null
+++ b/etc/sh/cronjob.sh
@@ -0,0 +1,74 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ cronjob COMMAND...
+ cronjob -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+ Options:
+ -h, --help show this message
+
+ COMMAND the command to be executed
+
+
+ Execute the given command, and send the output to email, with
+ special treatment to the status code.
+
+ It loads the appropriate files, so that the actual cron
+ declaration is smaller.
+
+
+ Examples:
+
+ Run a backup:
+
+ $ cronjob backup -q cron
+ 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))
+
+
+set +e
+# shellcheck disable=1090
+. ~/.profile
+set -e
+
+CMD="$*"
+with-email -s "$CMD" -- "$@" 1>>"$XDG_LOG_HOME"/euandreh/mcron.log 2>&1
diff --git a/etc/sh/privrc.sh b/etc/sh/privrc.sh
new file mode 120000
index 0000000..f946f34
--- /dev/null
+++ b/etc/sh/privrc.sh
@@ -0,0 +1 @@
+../../var/lib/private/tilde/privrc.sh \ No newline at end of file
diff --git a/etc/sh/rc b/etc/sh/rc
new file mode 100644
index 0000000..ae552a6
--- /dev/null
+++ b/etc/sh/rc
@@ -0,0 +1,364 @@
+#!/bin/sh
+# shellcheck disable=1090,1091
+
+export ENV=~/.profile
+
+export XDG_DATA_DIRS="$XDG_DATA_HOME/flatpak/exports/share:/var/lib/flatpak/exports/share${XDG_DATA_DIRS:+:}${XDG_DATA_DIRS:-}"
+
+
+mkdir -p \
+ "$XDG_CONFIG_HOME" \
+ "$XDG_CACHE_HOME/euandreh" \
+ "$XDG_DATA_HOME/euandreh" \
+ "$XDG_STATE_HOME/euandreh" \
+ "$XDG_LOG_HOME/euandreh"
+
+GUIX_PROFILE="$XDG_CONFIG_HOME"/guix/current
+if [ -r "$GUIX_PROFILE"/etc/profile ]; then
+ . "$GUIX_PROFILE"/etc/profile
+fi
+
+idempotent_path_add() {
+ case "$(eval "echo \$$1")" in
+ *"$2"*)
+ ;;
+ *)
+ eval "export $1=$2\${$1:+:}\${$1:-}"
+ ;;
+ esac
+}
+export A="$HOME${A:+:}${A:-}"
+# idempotent_path_add B "$HOME"
+export XDG_DATA_DIRS="$XDG_DATA_HOME/flatpak/exports/share:/var/lib/flatpak/exports/share${XDG_DATA_DIRS:+:}${XDG_DATA_DIRS:-}"
+# idempotent_path_add XDG_DATA_DIRS "$XDG_DATA_HOME/flatpak/exports/share"
+
+
+
+#
+#
+#
+
+export HISTSIZE=-1
+export HISTFILE="$XDG_STATE_HOME/sh-history"
+export SRC=~/dev
+export PRIV_CONFIG="$XDG_PREFIX"/var/lib/private/tilde
+export EDITOR='e'
+export VISUAL="$EDITOR"
+export PAGER='less -R'
+export BROWSER='firefox'
+export MAILDIR=~/Maildir
+export BACKLIGHT_DEVICE='acpi_video0'
+
+export GUILE_HISTORY="$XDG_STATE_HOME/guile-history"
+export GNUPGHOME="$XDG_CONFIG_HOME/gnupg"
+export RLWRAP_HOME="$XDG_CACHE_HOME/rlwrap"
+export LESSHISTFILE="$XDG_STATE_HOME/lesshst"
+export NOTMUCH_CONFIG="$XDG_CONFIG_HOME/notmuch/default/config"
+export PASSWORD_STORE_DIR="$SRC/private/password-store"
+export MAILCAPS="$XDG_CONFIG_HOME/mailcaps/config"
+export PYTHONSTARTUP="$XDG_CONFIG_HOME/python/pythonrc.py"
+export EXINIT='
+ " set number
+ " set autoindent
+ set ruler
+ set showmode
+ set showmatch
+'
+
+HOSTNAME="$(hostname)"
+export BORG_PASSCOMMAND="pass show $HOSTNAME/borg/passphrase"
+export BORG_REPO="suyin:borg/$HOSTNAME"
+export BORG_REMOTE_PATH='borg1'
+
+N_PROCS=$(($(nproc) * 2 + 1))
+GUILE_EFFECTIVE_VERSION="$(guile -c '(display (effective-version))')"
+export MAKEFLAGS="-j $N_PROCS"
+export GOPATH="$SRC/go"
+export CFLAGS='-std=c99 -Wall -Wextra -Wpedantic -g -flto -Werror'
+export CC=gcc
+export AR=gcc-ar
+export LEX=flex
+export LDFLAGS='-flto'
+export LISP='sbcl --eval'
+export N_PROCS GUILE_EFFECTIVE_VERSION
+export LISP_CLI_IMPL=clisp
+
+add_prefix() {
+ export GUILE_LOAD_PATH="$1/share/guile/site/$GUILE_EFFECTIVE_VERSION${GUILE_LOAD_PATH:+:}${GUILE_LOAD_PATH:-:}"
+ export GUILE_LOAD_COMPILED_PATH="$1/lib/guile/$GUILE_EFFECTIVE_VERSION/site-ccache${GUILE_LOAD_COMPILED_PATH:+:}${GUILE_LOAD_COMPILED_PATH:-}"
+ export C_INCLUDE_PATH="$1/include${C_INCLUDE_PATH:+:}${C_INCLUDE_PATH:-}"
+ export LIBRARY_PATH="$1/lib${LIBRARY_PATH:+:}${LIBRARY_PATH:-}"
+ export INFOPATH="$1/share/info${INFOPATH:+:}${INFOPATH:-}"
+ export MANPATH="$1/share/man${MANPATH:+:}${MANPATH:-}"
+ export DICPATH="$1/share/hunspell${DICPATH:+:}${DICPATH:-}"
+ export PATH="$1/bin${PATH:+:}${PATH:-}"
+}
+export PREFIX="$XDG_PREFIX/var/mkg"
+add_prefix "$PREFIX"
+add_prefix "$XDG_PREFIX"
+
+for d in "$XDG_PREFIX"/opt/bin-dirs/*; do
+ PATH="$d:$PATH"
+done
+
+
+
+#
+# Aliases
+#
+
+unalias -a
+
+alias p='ping euandre.org -c 3'
+alias c='tmux send-keys -R \; clear-history'
+alias o='open'
+alias mm='msmtp-queue -r'
+alias s='vcs status'
+alias d='vcs diff'
+alias ds='vcs diff -- --staged'
+alias tpd='cd "$(mkdtemp)"'
+
+alias l='ls -lahF --color'
+alias grep='grep --color=auto'
+alias diff='diff --color=auto'
+alias watch='watch --color '
+alias man='MANWIDTH=$((COLUMNS > 80 ? 80 : COLUMNS)) man'
+alias less='less -R'
+alias tree='tree -aC'
+alias make='make -e'
+alias mv='mv -i'
+alias vi='echo "Use \"e\" instead."; false'
+alias bc='bc -l'
+
+alias sqlite='rlwrap sqlite3'
+alias guile='guile -l "$XDG_CONFIG_HOME"/guile/init.scm'
+
+alias flush='sync && echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null'
+alias kal='khal calendar today `LANG=en.UTF-8 date +%A`'
+
+
+
+#
+# PS1
+#
+
+error_marker() {
+ STATUS=$?
+ if [ "$STATUS" != 0 ]; then
+ color -c redb " (!! $STATUS !!) "
+ fi
+}
+
+shell_status_level() {
+ if [ -z "${SHLVL:-}" ]; then
+ return
+ fi
+
+ if [ -n "${TMUX:-}" ]; then
+ LVL=$((SHLVL - 1))
+ else
+ LVL="$SHLVL"
+ fi
+
+ if [ "$LVL" = 1 ]; then
+ return
+ fi
+
+ color -c white "$LVL"
+ if [ -n "${RANGER_LEVEL:-}" ]; then
+ color -c white '|'
+ color -c bluei 'r'
+ fi
+}
+
+shell_status_jobs() {
+ JOBS="$(jobs | grep -cv autojump)"
+ if [ "$JOBS" != 0 ]; then
+ color -c red "$JOBS"
+ fi
+}
+
+shell_status() {
+ LEVEL="$(shell_status_level)"
+ JOBS="$(shell_status_jobs)"
+
+ if [ -z "$LEVEL" ] && [ -z "$JOBS" ]; then
+ return
+ fi
+
+ color -c white '['
+ printf '%s' "$LEVEL"
+ if [ -n "$LEVEL" ] && [ -n "$JOBS" ]; then
+ color -c white '|'
+ fi
+ printf '%s' "$JOBS"
+ color -c white ']'
+ printf ' '
+}
+
+timestamp() {
+ color -c blacki '\T'
+}
+
+path() {
+ color -c yellowb '\w/'
+}
+
+guix_env() {
+ if [ -z "${GUIX_ENVIRONMENT:-}" ]; then
+ return
+ fi
+
+ printf '\n'
+ color -c blacki '~> '
+ color -c purple 'guix environment '
+ printf '('
+ color -c blueb "$GUIX_ENVIRONMENT"
+ printf ')'
+}
+
+in_nix_shell() {
+ if [ -z "${IN_NIX_SHELL:-}" ]; then
+ return
+ fi
+
+ printf '\n'
+ color -c blacki '~> '
+ color -c purpleb "$IN_NIX_SHELL "
+ color -c purple 'nix-shell '
+ printf '('
+ color -c blueb "${name:-}"
+ printf ')'
+}
+
+PS1='`error_marker`'$(timestamp)' '$(path)' `shell_status``vcs ps1``guix_env``in_nix_shell`
+$ '
+
+
+
+# g '^\w.*json_destroy('
+g() {
+ # shellcheck disable=2086
+ fn=$(git grep -n -- "$1" ${2:-} | \
+ cut -d: -f -2 | \
+ fzf --select-1 \
+ --exit-0 \
+ --preview "echo {} | \
+ cut -d: -f1 | \
+ xargs -I% awk -v bounds=25 -v pat=\"$1\" -v n=\$(echo {} | cut -d: -f2) '
+ (n - bounds < NR) && (NR < n + bounds) && (NR != n) { print }
+ NR==n { gsub(pat, \"\033[1;33m&\033[1;000m\"); print }
+ ' %")
+ if [ -n "$fn" ]; then
+ f="$(echo "$fn" | cut -d: -f1)"
+ n="$(echo "$fn" | cut -d: -f2)"
+ # shellcheck disable=2068
+ # history -s g "$@"
+ # history -s vi "+$n" "$f"
+ # vi "+$n" "$f"
+ history -s g "$@"
+ history -s e "+$n" "$f"
+ e "+$n" "$f"
+ fi
+}
+
+f() {
+ # profile="f-shell-function$(pwd | sed -e 's_/_-_g')"
+ # file="$(git ls-files | grep ${2:-.} | remembering -p "$profile" -c "fzf --select-1 --exit-0 --preview 'cat {}'")"
+ # shellcheck disable=2086
+ file="$(git ls-files | grep ${2:-.} | fzf --select-1 --exit-0 --preview 'cat {}')"
+ if [ -n "$file" ]; then
+ # shellcheck disable=2068
+ history -s f "$@"
+ history -s "$1" "$file"
+ "$1" "$file"
+ fi
+}
+
+
+
+r() {
+ . ~/.profile
+ xset r rate 225 100
+}
+
+_edit_without_executing() {
+ F="$(mkstemp)"
+ printf '%s\n' "$READLINE_LINE" > "$F"
+ e "$F"
+ READLINE_LINE="$(cat "$F")"
+ READLINE_POINT="${#READLINE_LINE}"
+ rm -f "$F"
+}
+if set -o | grep 'on' | grep -Eq '^(vi|emacs)'; then
+ bind -x '"\C-x\C-e":_edit_without_executing'
+fi
+
+
+
+
+#
+# GPG and SSH agents configuration
+#
+
+SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
+GPG_TTY=$(tty ||:)
+export GPG_TTY SSH_AUTH_SOCK
+gpgconf --launch gpg-agent
+
+
+
+case $- in
+ *i*)
+ stty -ixon # Disable C-s/C-q mode
+ ;;
+ *)
+ ;;
+esac
+
+F="$PRIV_CONFIG"/privrc.sh
+if [ -r "$F" ]; then
+ # Extra rc code to be loaded, stored in private repository
+ . "$F"
+fi
+
+
+
+#
+# From here on, bash-specific things.
+#
+
+if [ -z "$BASH_VERSION" ]; then
+ return
+fi
+
+ln -fs .profile ~/.bashrc
+
+export INPUTRC="$XDG_CONFIG_HOME/bash/inputrc"
+export HISTCONTROL=ignorespace:ignoredups
+
+eval "$(direnv hook bash)"
+
+
+#
+# From here on, interactive-only bash-specific things.
+#
+
+case $- in
+ *i*)
+ ;;
+ *)
+ return
+ ;;
+esac
+
+# IIRC, the Guix profile should do this
+for f in "$HOME_ENVIRONMENT"/profile/etc/bash_completion.d/*; do
+ . "$f"
+done
+
+# IIRC, the Guix package should do this
+F="$HOME_ENVIRONMENT"/profile/share/autojump/autojump.bash
+if [ -r "$F" ]; then
+ . "$F"
+fi
diff --git a/etc/sh/root-rc b/etc/sh/root-rc
new file mode 100644
index 0000000..f78f293
--- /dev/null
+++ b/etc/sh/root-rc
@@ -0,0 +1,87 @@
+#!/bin/sh
+# shellcheck disable=1090,1091
+export ENV=~/.profile
+
+XDG_PREFIX=~/.usr
+export XDG_CACHE_HOME="$XDG_PREFIX/var/cache"
+export XDG_CONFIG_HOME="$XDG_PREFIX/etc"
+export XDG_DATA_HOME="$XDG_PREFIX/share"
+export XDG_STATE_HOME="$XDG_PREFIX/state"
+export XDG_LOG_HOME="$XDG_PREFIX/var/log"
+
+mkdir -p \
+ "$XDG_CACHE_HOME" \
+ "$XDG_CONFIG_HOME" \
+ "$XDG_DATA_HOME" \
+ "$XDG_STATE_HOME" \
+ "$XDG_LOG_HOME"
+
+GUIX_PROFILE="$XDG_CONFIG_HOME"/guix/current
+if [ -e "$GUIX_PROFILE"/etc/profile ]; then
+ . "$GUIX_PROFILE"/etc/profile
+fi
+
+HISTSIZE=-1
+HISTFILE="$XDG_STATE_HOME"/bash-history
+HISTCONTROL=ignorespace:ignoredups
+
+export EDITOR='vi'
+export VISUAL="$EDITOR"
+export PAGER='less -R'
+export GUILE_HISTORY="$XDG_STATE_HOME"/guile-history
+export RLWRAP_HOME="$XDG_CACHE_HOME"/rlwrap
+export LESSHISTFILE="$XDG_STATE_HOME"/lesshst
+export EXINIT='
+ " set number
+ set autoindent
+ set ruler
+ set showmode
+ set showmatch
+'
+
+alias l='ls -lahF --color=auto'
+alias grep='grep --color=auto'
+alias diff='diff --color'
+alias less='less -R'
+alias mv='mv -i'
+alias rm='rm -i'
+alias r='reload'
+
+reload() {
+ . ~/.profile
+}
+
+
+
+#
+# PS1
+#
+
+error_marker() {
+ STATUS=$?
+ if [ "$STATUS" != 0 ]; then
+ printf ' (!! %s !!) ' "$STATUS"
+ fi
+}
+
+shell_level() {
+ # shellcheck disable=3028
+ L="${SHLVL:-1}"
+ if [ -n "${TMUX:-}" ]; then
+ LVL=$((L - 1))
+ else
+ LVL="$L"
+ fi
+ if [ "$LVL" != 1 ]; then
+ printf '[%s] ' "$LVL"
+ fi
+}
+
+guix_env() {
+ if [ -n "${GUIX_ENVIRONMENT:-}" ]; then
+ printf '\n~> guix environment (%s)' "$GUIX_ENVIRONMENT"
+ fi
+}
+
+PS1='`error_marker`\T \w/ `shell_level``guix_env`
+# '
diff --git a/etc/ssh/config.tmpl b/etc/ssh/config.tmpl
new file mode 100644
index 0000000..fd4b8a3
--- /dev/null
+++ b/etc/ssh/config.tmpl
@@ -0,0 +1,6 @@
+Host *
+ UserKnownHostsFile ${XDG_CONFIG_HOME}/ssh/known_hosts
+
+Include ~/dev/libre/servers/src/infrastructure/ssh.conf
+Include ~/dev/others/lawtech/src/infrastructure/ssh.conf
+Include ${XDG_DATA_HOME}/euandreh/vm-ssh.conf
diff --git a/etc/ssh/known_hosts b/etc/ssh/known_hosts
new file mode 100644
index 0000000..fa1b43a
--- /dev/null
+++ b/etc/ssh/known_hosts
@@ -0,0 +1,84 @@
+|1|G2vfdmQ84glwobYXZZ0d+cCMVRE=|DZJYmWjbeP52J4K7+Bsz2e0dgBA= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
+|1|yosAhKlbZt51FfD2VvQJiVijSBA=|BFEig1gqq4EwCHEHagEASQZQmNI= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
+|1|5yKbUaAB5AFz4MINtTVhVAPwMtU=|LwcI5Z8hXwwKxtkk4KDpeZduyPQ= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+|1|sfEM3JnFec93XEzMf9A6TkthgdI=|oZX7oe9eBKYMrXLcoFydh70my5A= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+|1|F/E3nafzrpMZEzyN8iA++okJ7Q4=|T00Gk7F90YmbaTMSOGjP8yhls94= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMylg2eib0byVT2R7dVFkxdhXO5pvgllnszKhlHiEP15ee8IjMYNPvz2A605hUdIxXtsBgjf+u7jlubh6mbx/YA=
+|1|jSpDKjDomux2z3O/ok/UPGGQ8xs=|ZP60naGKQnpK6yHGD+B/+ykB05c= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMylg2eib0byVT2R7dVFkxdhXO5pvgllnszKhlHiEP15ee8IjMYNPvz2A605hUdIxXtsBgjf+u7jlubh6mbx/YA=
+|1|PJybLcZRkpN9IyDsqaNjGO6lE5Y=|K0nchvCA7XV91J2X6l0h2DwOmyI= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+|1|zzdSJf8JIMfi5sKbc/mNcBy/RB8=|0GxC/CesxJHcRdt8MuPVjfab06k= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBxDZv64oRMzRkywjmRRrml2pr0XFSZhlL46nUSmM60
+|1|X5OmtKdXZh2kC//XCXEEOim7tgE=|Rh5ro2oEB4MN8MP6PRbG3QBR0Kk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBxDZv64oRMzRkywjmRRrml2pr0XFSZhlL46nUSmM60
+46.101.43.82 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLIGIc5X12Y61eVcKJnNzCvrPeSKgyKgElqGl7QDeGeynE33sRVoNAg9aqkgXdc2MkN+nFhEWELkjPuBKYsyp2I=
+hinarioespirita.org,167.99.34.30 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEnGFkXNEF6RHihT3szgVEBAhRKXlI6YKSUOQFzhuNVkMOBgEfr+OaXpFV3zQ3/Dp875skdTOZaA9DjQ0EeUS+M=
+pt.hinarioespirita.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEnGFkXNEF6RHihT3szgVEBAhRKXlI6YKSUOQFzhuNVkMOBgEfr+OaXpFV3zQ3/Dp875skdTOZaA9DjQ0EeUS+M=
+mtm.hinarioespirita.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEnGFkXNEF6RHihT3szgVEBAhRKXlI6YKSUOQFzhuNVkMOBgEfr+OaXpFV3zQ3/Dp875skdTOZaA9DjQ0EeUS+M=
+77.109.148.18 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBxDZv64oRMzRkywjmRRrml2pr0XFSZhlL46nUSmM60
+35.231.145.151 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
+git.sr.ht,173.195.146.142 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCj6y+cJlqK3BHZRLZuM+KP2zGPrh4H66DacfliU1E2DHAd1GGwF4g1jwu3L8gOZUTIvUptqWTkmglpYhFp4Iy4=
+192.168.33.10 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCL+hEiW/Va2L6y5Y4MyxRBXHJw/9bIC02M3wGVe1zaD3DT8wsAUcJ2QO1lJILudvMInx+SaPyJmwBvUn58YR2c=
+173.195.146.152 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCj6y+cJlqK3BHZRLZuM+KP2zGPrh4H66DacfliU1E2DHAd1GGwF4g1jwu3L8gOZUTIvUptqWTkmglpYhFp4Iy4=
+2001:1620:2019::218 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBxDZv64oRMzRkywjmRRrml2pr0XFSZhlL46nUSmM60
+140.82.114.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+140.82.113.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+140.82.113.4 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+140.82.114.4 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+2604:bf00:710:0:5054:ff:fe7d:8fa8 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCj6y+cJlqK3BHZRLZuM+KP2zGPrh4H66DacfliU1E2DHAd1GGwF4g1jwu3L8gOZUTIvUptqWTkmglpYhFp4Iy4=
+azusa.runners.sr.ht,2604:bf00:710:0:ae1f:6bff:fead:55a ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzxK5AM1FdMI9gZVDpw2O5iiS/49QokWpzANFntVt0Qig4qdBt4K7B0O6MrwggLh3A+zBlsXCMoWtvFtPQgLxA=
+18.228.52.138 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+18.231.5.6 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+2604:bf00:710:0:5054:ff:fe36:ebc6 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCj6y+cJlqK3BHZRLZuM+KP2zGPrh4H66DacfliU1E2DHAd1GGwF4g1jwu3L8gOZUTIvUptqWTkmglpYhFp4Iy4=
+18.228.67.229 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+hg.sr.ht,2604:bf00:710:0:5054:ff:fe25:1aa6 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL4aNGa+KnvMA0QoWrIVuI2QBU0Q/xX48sMBl3VtP/zPOGMvS50zGVMaA00RSzfcI2X0v/aUTsVm2vBNo/V1gTg=
+euandre.org,2a03:b0c0:3:d0::387:b001 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBF6wlJqwd7KWLzLovwnwbTmNfO4E4yVDyqxAAlUkn9eDsTtzV1RYNDsaLPWv4mweJqP4crZPFxg40sFVeMDbkC0=
+2606:4700:90:0:f22e:fbec:5bed:a9b9 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
+46.101.160.115 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBF6wlJqwd7KWLzLovwnwbTmNfO4E4yVDyqxAAlUkn9eDsTtzV1RYNDsaLPWv4mweJqP4crZPFxg40sFVeMDbkC0=
+173.195.146.249 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzxK5AM1FdMI9gZVDpw2O5iiS/49QokWpzANFntVt0Qig4qdBt4K7B0O6MrwggLh3A+zBlsXCMoWtvFtPQgLxA=
+2604:a880:800:14::32:4000 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBK8YTumqLFAL5MJ0AcDtFG9dWfbkJKU7FUDlH0xEgAZvHGU57TBr9DIQy2OHrxCxuhk9bZEUX8+vJiRXE05+Rzs=
+2001:19f0:7001:5cec:5400:2ff:feec:9940 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPYRWlGutc+bRJ+N0sXHxhgnDsRvRoauQ92yM1U7N+8a
+2001:19f0:6c01:2cf0:5400:2ff:feec:99c5 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMGiiEXB+koPS1vgGkwdExk2Q5fGv3Yc5rf8jVHB2FB7
+45.32.155.96 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfPxSXxvq3MHuMvthg+q69ooniSeqXbit1UiW0gbLZN
+45.77.65.204 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfPxSXxvq3MHuMvthg+q69ooniSeqXbit1UiW0gbLZN
+136.244.85.68 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfPxSXxvq3MHuMvthg+q69ooniSeqXbit1UiW0gbLZN
+199.247.0.136 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+euandreh.xyz ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+136.244.80.130 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+209.250.232.122 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+git.euandreh.xyz ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+80.240.24.148 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+95.179.246.150 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+45.77.52.185 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+199.247.2.245 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+140.82.112.4 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+104.238.176.223 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+172.65.251.78 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
+140.82.112.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+45.32.158.17 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+95.179.253.243 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+95.179.163.103 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+217.69.2.177 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhpYZqFXy4CkOrSg5naR5Any47jqgZUIwbiTvsl2Yhm5EiUtCZTIVHui7q262M5qlDY6syQ5lVMtSsLJuTce+I=
+104.238.176.81 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhpYZqFXy4CkOrSg5naR5Any47jqgZUIwbiTvsl2Yhm5EiUtCZTIVHui7q262M5qlDY6syQ5lVMtSsLJuTce+I=
+173.199.70.52 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhpYZqFXy4CkOrSg5naR5Any47jqgZUIwbiTvsl2Yhm5EiUtCZTIVHui7q262M5qlDY6syQ5lVMtSsLJuTce+I=
+[remembering.euandreh.xyz]:23841 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+167.71.86.194 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCXg/l/grol/OAY95VphKbVn4yXUStSWpxQjnka7PWXzWyForfX+fpmY+p72r1Uimx5oXIFISt8uSZW/tvD61iQ=
+[localhost]:10022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLHACdQJXUj5e4Znyh7KdTjB1RLznSfStRw6cuHZlu8rhANVfkjt300Fum9Jv6yLra6W4v2FLALWfcYpOlUzt9c=
+107.191.63.70 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMONpsqwH79f/MsjtsOPitT5C+3hPPJqVh42oHMKOen6
+217.69.11.49 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKbzWSlFwmlrC+k6XU9eUJaejREGxTF03OX/mT7KhSr
+anoncvs.netbsd.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA3QiBl8leG9fqIJpKeNov0PKq5YryFFiroMWOPUv4hDFn8R0jC07YVaR/OSBrr37CTmGX5AFceXPzoFnLlwCqWR7rXg4NR75FTlTp9CG9EBAEtU8mee27KDrUFBTZdfVl2+aRYoAI5fTXA+0vpIO68Cq843vRWUZCcwinS4cNLUU=
+git.2f30.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJSLZ4G5w4NysBUmAHmr6/V9om42IHSUCtqrNdhWoYQ0
+[199.247.13.53]:38123 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoz1gFl6chY91vQ5SrZXSP5yHqRI3TdYy2ccEDpS7Z4
+[2001:19f0:6801:988:5400:3ff:fea1:b566]:38123 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoz1gFl6chY91vQ5SrZXSP5yHqRI3TdYy2ccEDpS7Z4
+[gerrit.wikimedia.org]:29418 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCF8pwFLehzCXhbF1jfHWtd9d1LFq2NirplEBQYs7AOrGwQ/6ZZI0gvZFYiEiaw1o+F1CMfoHdny1VfWOJF3mJ1y9QMKAacc8/Z3tG39jBKRQCuxmYLO1SWymv7/Uvx9WQlkNRoTdTTa9OJFy6UqvLQEXKYaokfMIUHZ+oVFf1CgQ==
+2001:19f0:5:1d65:5400:3ff:fee3:7463 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF2EgU3IgCwv1ynnWfxFm0SHTSoE0AYG3MJT/TpN3pBz
+[camarada.site]:38123 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoz1gFl6chY91vQ5SrZXSP5yHqRI3TdYy2ccEDpS7Z4
+149.28.21.56 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2cITa9TuC3lzEihIfFYb0KhyJJ5Vg2vnGW1SMMwxhc
+camarada.site ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoz1gFl6chY91vQ5SrZXSP5yHqRI3TdYy2ccEDpS7Z4
+[127.0.0.1]:60022 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuoUWqrJ8WLBDWwWG7zCyYVYz1upMlg1mSXMGMHIVzY
+[localhost]:60022 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuoUWqrJ8WLBDWwWG7zCyYVYz1upMlg1mSXMGMHIVzY
+[10.0.2.1]:23841 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+[kuvira.wg]:23841 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+kuvira.wg ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvhhXQBEY9GhWOtBoLpDJRjqHAu7ci2A9l1xoet1Cz/
+216.238.68.100 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKF5klRzMiaDhfFv7vj6nIT2BdjbcgpsmnNT3y/X9oUu
+arrobaponto.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKF5klRzMiaDhfFv7vj6nIT2BdjbcgpsmnNT3y/X9oUu
+10.0.2.2 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9jxIXM2FSYeZmY2uLWkQUJQLNIQQJyJdc7P4eEPhEU
+velhinho ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9jxIXM2FSYeZmY2uLWkQUJQLNIQQJyJdc7P4eEPhEU
+velhinho.wg ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9jxIXM2FSYeZmY2uLWkQUJQLNIQQJyJdc7P4eEPhEU
+localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9jxIXM2FSYeZmY2uLWkQUJQLNIQQJyJdc7P4eEPhEU
diff --git a/etc/tmux/tmux.conf b/etc/tmux/tmux.conf
new file mode 100644
index 0000000..93efdf9
--- /dev/null
+++ b/etc/tmux/tmux.conf
@@ -0,0 +1,96 @@
+# Use personal prefix over "C-b"
+unbind C-b
+set -g prefix C-v
+
+# Turn on mouse mode
+# https://groups.google.com/forum/#!msg/tmux-users/TRwPgEOVqho/Ck_oth_SDgAJ
+# https://github.com/tmux/tmux/blob/310f0a960ca64fa3809545badc629c0c166c6cd2/CHANGES#L12
+set -g mouse on
+
+# Bind "C-x r" to reload the configuration file
+bind-key r source-file $XDG_CONFIG_HOME/tmux/tmux.conf \; display-message "$XDG_CONFIG_HOME/tmux.conf reloaded"
+
+# Holy answer that properly implements copying from tmux!
+# https://unix.stackexchange.com/a/349020/276661
+bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe "xclip -selection clipboard -i" \;
+bind-key y send-keys -X copy-pipe "xclip -selection clipboard -i" \; display-message "Copied system clipboard! Hooray! Long-live tmux!"
+
+# Moving around windows
+bind-key -n M-[ previous-window
+bind-key -n M-] next-window
+
+# Moving around panes
+bind-key -n M-h select-pane -L
+bind-key -n M-j select-pane -D
+bind-key -n M-k select-pane -U
+bind-key -n M-l select-pane -R
+
+# Resizing panes
+bind-key -n M-H resize-pane -L 5
+bind-key -n M-J resize-pane -D 5
+bind-key -n M-K resize-pane -U 5
+bind-key -n M-L resize-pane -R 5
+
+# Reorder windows
+bind-key -n C-S-Left swap-window -t -1\; select-window -t -1
+bind-key -n C-S-Right swap-window -t +1\; select-window -t +1
+#
+# To change the number of a window, use: PREFIX-., and pick a new unused number
+#
+
+# Join windows
+bind-key -n C-S-M-Left join-pane -s :-0 -t :-1
+bind-key -n C-S-M-Right join-pane -s :-0 -t :+1
+
+# "M m" to actually clear the pane history
+bind -n M-m send-keys -R \; clear-history
+
+# Keybinding to activate pane typing sync
+# https://stackoverflow.com/questions/25909964/tmux-how-to-toggle-on-and-off-options-with-the-same-key
+bind-key b setw synchronize-panes \; display-message "synchronize-panes toggle"
+
+setw -g mode-keys vi # Move around with vi keys
+set-option -g status-key "vi" # Use vi mode for status bar command (like after typing "C-x [" one can search with "/")
+set-option -g status-bg "#333333" # Status bar background color
+set-option -g status-fg "#ffffff" # Status bar foreground color
+set-option -g status-left-length 50 # session name in status bar length =[annex]= part
+set-option -g history-limit 150000 # How many lines of history to keep
+set-option -g status-right ""
+
+# Set the panes initial index value to 1 instead of 0
+# 0 is too far from ` ;)
+set -g base-index 1
+set-window-option -g pane-base-index 1
+
+# Automatically set window title
+set-window-option -g automatic-rename on
+set-option -g set-titles on
+
+# Set "correct term"
+# https://wiki.archlinux.org/index.php/Tmux
+set -g default-terminal screen-256color
+
+# No delay for escape key press
+# https://mutelight.org/practical-tmux#faster-command-sequences
+set -sg escape-time 0
+
+# Display pane numbers for longer
+# https://unix.stackexchange.com/questions/307696/how-to-increase-tmux-pane-numbers-display-time-ctrl-b-q
+set -g display-panes-time 2500
+
+bind-key t resize-pane -x 80
+
+bind -n M-r attach-session -t . -c '#{pane_current_path}' \; display-message "CWD for session updated to #{pane_current_path}!"
+
+bind-key u capture-pane \; save-buffer $XDG_RUNTIME_DIR/tmux-urlscan-buffer \; new-window -n "urlscan" '$SHELL -c "urlscan < $XDG_RUNTIME_DIR/tmux-urlscan-buffer"'
+bind-key - last
+
+
+# $XDG_DATA_HOME variable isn't allowed by tmux-resurrect
+set -g @resurrect-dir '~/.usr/share/tmux/resurrect'
+set -g @continuum-restore 'on'
+set -g @continuum-save-interval '60'
+set -g @resurrect-capture-pane-contents 'on'
+set -g @resurrect-processes '~make ~ssh ~e "~alot->alot" "~ranger->ranger" "~newsboat->newsboat" "~entr->entr" "~git->git" "~info->info"'
+run-shell ~/.guix-home/profile/share/tmux-plugins/resurrect/resurrect.tmux
+run-shell ~/.guix-home/profile/share/tmux-plugins/continuum/continuum.tmux
diff --git a/etc/weechat/irc.conf b/etc/weechat/irc.conf
new file mode 120000
index 0000000..d42fdad
--- /dev/null
+++ b/etc/weechat/irc.conf
@@ -0,0 +1 @@
+../../var/lib/private/tilde/weechat/irc.conf \ No newline at end of file
diff --git a/opt/aux/gen-e-list.sh b/opt/aux/gen-e-list.sh
new file mode 100755
index 0000000..3d5ee4b
--- /dev/null
+++ b/opt/aux/gen-e-list.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+set -eu
+
+{
+ cat <<-EOF
+ ~/Documents/txt/TODOs.md
+ ~/Documents/txt/scratch.txt
+ $XDG_CONFIG_HOME/sh/rc
+ $XDG_CONFIG_HOME/guix/home.scm
+ $XDG_CONFIG_HOME/guix/system.scm
+ EOF
+
+ find ~/Documents/txt/*.txt ~/Documents/txt/*.md -not -name TODOs.md |
+ sed "s|^$HOME|~|" |
+ LANG=POSIX.UTF-8 sort
+
+ cat <<-EOF
+ ~/dev/libre/package-repository/dependencies.dot
+ ~/dev/private/armário/src/dinheiros/dinheiros.ledger
+ ~/dev/libre/dotfiles/sh/fake-symlinks.sh
+ $XDG_CONFIG_HOME/guix/channels.scm
+ $XDG_CONFIG_HOME/lisp-cli/init.lisp
+ EOF
+} | sed "s|$HOME|~|"
diff --git a/opt/bin-dirs/clisp b/opt/bin-dirs/clisp
new file mode 120000
index 0000000..56fe4cc
--- /dev/null
+++ b/opt/bin-dirs/clisp
@@ -0,0 +1 @@
+/home/andreh/dev/misc/common-lisp/clisp/src \ No newline at end of file
diff --git a/opt/bin-dirs/euandre.org b/opt/bin-dirs/euandre.org
new file mode 120000
index 0000000..db5a280
--- /dev/null
+++ b/opt/bin-dirs/euandre.org
@@ -0,0 +1 @@
+/home/andreh/dev/published/euandre.org/src/bin \ No newline at end of file
diff --git a/opt/tests/assert-gpg-expiration.sh b/opt/tests/assert-gpg-expiration.sh
new file mode 100755
index 0000000..d17486e
--- /dev/null
+++ b/opt/tests/assert-gpg-expiration.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+
+
+SECRET_KEY='81F90EC3CD356060'
+NEXT_6_MONTHS="$(echo "$(date '+%s') + (60 * 60 * 24 * 30 * 6)" | bc)"
+
+gpg --with-colons --fixed-list-mode --list-keys "$SECRET_KEY" |
+ grep -e ^pub -e ^sub |
+ while read -r subkey; do
+ EXPIRY="$(echo "$subkey" | cut -d: -f7)"
+ if [ -z "$EXPIRY" ]; then
+ continue
+ fi
+
+ if [ "$EXPIRY" -gt "$(date '+%s')" ] &&
+ [ "$EXPIRY" -lt "$NEXT_6_MONTHS" ]; then
+ printf 'Key %s to expire soon!.\n' \
+ "$(echo "$subkey" | cut -d: -f5)" >&2
+ exit 1
+ fi
+ done
diff --git a/share/msg/bad.ogg b/share/msg/bad.ogg
new file mode 100644
index 0000000..0e91525
--- /dev/null
+++ b/share/msg/bad.ogg
Binary files differ
diff --git a/share/msg/good.ogg b/share/msg/good.ogg
new file mode 100644
index 0000000..22cb563
--- /dev/null
+++ b/share/msg/good.ogg
Binary files differ