about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--COPYING17
-rw-r--r--LICENSE674
-rw-r--r--README.md13
-rw-r--r--default.nix13
-rw-r--r--doc/entities.ent22
-rw-r--r--doc/index.xml37
-rw-r--r--doc/install.xml81
-rw-r--r--doc/options.xml9
-rw-r--r--lib/call-machine.nix73
-rw-r--r--lib/call-network.nix18
-rw-r--r--lib/default.nix5
-rw-r--r--lib/get-tests.nix24
-rw-r--r--machines/README.md4
-rw-r--r--machines/aszlig/dnyarri.nix139
-rw-r--r--machines/aszlig/managed/brawndo.nix54
-rw-r--r--machines/aszlig/managed/shakti.nix72
-rw-r--r--machines/aszlig/managed/tyree.nix72
-rw-r--r--machines/aszlig/meshuggah.nix56
-rw-r--r--machines/aszlig/tishtushi.nix63
-rw-r--r--machines/default.nix28
-rw-r--r--machines/devhell/eir.nix254
-rw-r--r--machines/devhell/gunnr.nix158
-rw-r--r--machines/devhell/hildr.nix215
-rw-r--r--machines/devhell/sigrun.nix280
-rw-r--r--machines/misc/mailserver.nix118
-rw-r--r--machines/openlab/manual-setup.md24
-rw-r--r--machines/profpatsch/base-server.nix36
-rw-r--r--machines/profpatsch/base-workstation.nix155
-rw-r--r--machines/profpatsch/base.nix61
-rw-r--r--machines/profpatsch/haku.nix210
-rw-r--r--machines/profpatsch/lib.nix17
-rw-r--r--machines/profpatsch/mikiya.nix90
-rw-r--r--machines/profpatsch/patches/libnotify.patch39
-rw-r--r--machines/profpatsch/pkgs.nix44
-rw-r--r--machines/profpatsch/shiki.nix439
-rw-r--r--machines/sternenseemann/fliewatuet.nix275
-rw-r--r--machines/sternenseemann/patches/2bwm-config.patch131
-rw-r--r--machines/sternenseemann/pkgs.nix13
-rw-r--r--machines/sternenseemann/schaf.nix105
-rw-r--r--machines/sternenseemann/schnurrkadse.nix185
-rw-r--r--modules/README.md49
-rw-r--r--modules/core/common.nix74
-rw-r--r--modules/core/lazy-packages.nix67
-rw-r--r--modules/core/licensing.nix19
-rw-r--r--modules/core/tests.nix1137
-rw-r--r--modules/hardware/gamecontroller.nix109
-rw-r--r--modules/hardware/rtl8192cu/default.nix50
-rw-r--r--modules/hardware/t100ha/brcmfmac43340-sdio.txtbin0 -> 3239 bytes
-rw-r--r--modules/hardware/t100ha/default.nix97
-rw-r--r--modules/hardware/thinkpad.nix31
-rw-r--r--modules/module-list.nix36
-rw-r--r--modules/programs/fish/fasd.nix30
-rw-r--r--modules/programs/gnupg/agent-wrapper.c313
-rw-r--r--modules/programs/gnupg/default.nix191
-rw-r--r--modules/programs/gnupg/pinentry-wrapper.c281
-rw-r--r--modules/services/guix.nix106
-rw-r--r--modules/services/postfix/default.nix65
-rw-r--r--modules/services/starbound.nix351
-rw-r--r--modules/system/iso.nix12
-rw-r--r--modules/system/kernel/bfq/bfq-by-default-4.15.patch13
-rw-r--r--modules/system/kernel/bfq/bfq-by-default-4.18.patch13
-rw-r--r--modules/system/kernel/bfq/bfq-by-default-5.4.patch13
-rw-r--r--modules/system/kernel/bfq/bfq-by-default.patch13
-rw-r--r--modules/system/kernel/bfq/default.nix36
-rw-r--r--modules/system/kernel/rckernel.nix23
-rw-r--r--modules/system/kernel/zswap.nix37
-rw-r--r--modules/user/aszlig/profiles/base.nix97
-rw-r--r--modules/user/aszlig/profiles/managed.nix115
-rw-r--r--modules/user/aszlig/profiles/workstation/default.nix186
-rw-r--r--modules/user/aszlig/profiles/workstation/lazy-packages.nix25
-rw-r--r--modules/user/aszlig/profiles/workstation/packages.nix85
-rw-r--r--modules/user/aszlig/programs/git/default.nix70
-rw-r--r--modules/user/aszlig/programs/mpv/default.nix24
-rw-r--r--modules/user/aszlig/programs/taskwarrior/config.patch48
-rw-r--r--modules/user/aszlig/programs/taskwarrior/default.nix36
-rw-r--r--modules/user/aszlig/programs/zsh/default.nix129
-rw-r--r--modules/user/aszlig/services/i3/conky.nix122
-rw-r--r--modules/user/aszlig/services/i3/default.nix132
-rw-r--r--modules/user/aszlig/services/i3/i3.conf131
-rw-r--r--modules/user/aszlig/services/i3/workspace.nix107
-rw-r--r--modules/user/aszlig/services/vlock/default.nix67
-rw-r--r--modules/user/aszlig/services/vlock/message.cat18
-rw-r--r--modules/user/aszlig/services/vlock/message.colmap18
-rw-r--r--modules/user/devhell/profiles/base.nix128
-rw-r--r--modules/user/devhell/profiles/packages.nix283
-rw-r--r--modules/user/devhell/profiles/services.nix102
-rw-r--r--modules/user/openlab/base.nix83
-rw-r--r--modules/user/openlab/labtops.nix103
-rw-r--r--modules/user/openlab/speedtest.nix48
-rwxr-xr-xmodules/user/openlab/speedtest.py66
-rw-r--r--modules/user/openlab/stackenblocken.nix39
-rw-r--r--modules/user/profpatsch/programs/scanning.nix29
-rw-r--r--nixpkgs-path.nix2
-rwxr-xr-xpkgs/aszlig/aacolorize/aacolorize.py182
-rw-r--r--pkgs/aszlig/aacolorize/default.nix13
-rw-r--r--pkgs/aszlig/axbo/default.nix53
-rw-r--r--pkgs/aszlig/default.nix15
-rw-r--r--pkgs/aszlig/git-detach/default.nix33
-rw-r--r--pkgs/aszlig/gopass/ascii-symbols.patch17
-rw-r--r--pkgs/aszlig/gopass/default.nix8
-rw-r--r--pkgs/aszlig/gopass/use-color-in-pager.patch20
-rw-r--r--pkgs/aszlig/grandpa/default.nix20
-rw-r--r--pkgs/aszlig/librxtx-java/default.nix47
-rw-r--r--pkgs/aszlig/lockdev/default.nix23
-rw-r--r--pkgs/aszlig/psi/config.patch278
-rw-r--r--pkgs/aszlig/psi/darkstyle.patch32
-rw-r--r--pkgs/aszlig/psi/default.nix73
-rw-r--r--pkgs/aszlig/psi/disable-jingle.patch12
-rw-r--r--pkgs/aszlig/psi/disable-xep-0232.patch50
-rw-r--r--pkgs/aszlig/pvolctrl/default.nix35
-rw-r--r--pkgs/aszlig/vim/default.nix523
-rw-r--r--pkgs/aszlig/xournal/aspect-ratio.patch83
-rw-r--r--pkgs/aszlig/xournal/default.nix5
-rw-r--r--pkgs/build-support/build-sandbox/default.nix107
-rw-r--r--pkgs/build-support/build-sandbox/src/Makefile32
-rw-r--r--pkgs/build-support/build-sandbox/src/nix-query.cc118
-rw-r--r--pkgs/build-support/build-sandbox/src/nix-query.h6
-rw-r--r--pkgs/build-support/build-sandbox/src/params.h10
-rw-r--r--pkgs/build-support/build-sandbox/src/path-cache.cc21
-rw-r--r--pkgs/build-support/build-sandbox/src/path-cache.h10
-rw-r--r--pkgs/build-support/build-sandbox/src/sandbox.c21
-rw-r--r--pkgs/build-support/build-sandbox/src/setup.c907
-rw-r--r--pkgs/build-support/build-sandbox/src/setup.h15
-rw-r--r--pkgs/build-support/channel.nix32
-rw-r--r--pkgs/default.nix32
-rw-r--r--pkgs/games/build-support/build-game.nix59
-rw-r--r--pkgs/games/build-support/build-unity.nix80
-rw-r--r--pkgs/games/build-support/default.nix11
-rw-r--r--pkgs/games/build-support/monogame-patcher/default.nix27
-rw-r--r--pkgs/games/build-support/monogame-patcher/src/assembly-info.cs6
-rw-r--r--pkgs/games/build-support/monogame-patcher/src/options.cs34
-rw-r--r--pkgs/games/build-support/monogame-patcher/src/patcher.cs202
-rw-r--r--pkgs/games/build-support/monogame-patcher/src/patcher.csproj43
-rw-r--r--pkgs/games/build-support/monogame-patcher/src/test.sh116
-rw-r--r--pkgs/games/build-support/setup-hooks/default.nix13
-rw-r--r--pkgs/games/build-support/setup-hooks/fix-fmod.sh68
-rw-r--r--pkgs/games/build-support/setup-hooks/gog-unpack.sh71
-rw-r--r--pkgs/games/default.nix51
-rw-r--r--pkgs/games/gog/albion/cdpath-is-gamedir.patch66
-rw-r--r--pkgs/games/gog/albion/config.patch19
-rw-r--r--pkgs/games/gog/albion/default.nix187
-rw-r--r--pkgs/games/gog/albion/error-log-stderr.patch100
-rw-r--r--pkgs/games/gog/albion/scons.patch74
-rw-r--r--pkgs/games/gog/albion/sdl2.patch60
-rw-r--r--pkgs/games/gog/albion/storepaths.patch15
-rw-r--r--pkgs/games/gog/albion/wildmidi-build-fixes.patch25
-rw-r--r--pkgs/games/gog/albion/xdg-paths.patch259
-rw-r--r--pkgs/games/gog/crosscode.nix32
-rw-r--r--pkgs/games/gog/default.nix58
-rw-r--r--pkgs/games/gog/dungeons3.nix16
-rw-r--r--pkgs/games/gog/epistory.nix35
-rw-r--r--pkgs/games/gog/fetch-gog/default.nix316
-rw-r--r--pkgs/games/gog/fetch-gog/hexify-char.nix258
-rw-r--r--pkgs/games/gog/homm3/default.nix97
-rw-r--r--pkgs/games/gog/homm3/launcher-execl.patch36
-rw-r--r--pkgs/games/gog/kingdoms-and-castles.nix14
-rw-r--r--pkgs/games/gog/overload.nix14
-rw-r--r--pkgs/games/gog/party-hard.nix14
-rw-r--r--pkgs/games/gog/planescape-torment-enhanced-edition.nix32
-rw-r--r--pkgs/games/gog/satellite-reign.nix16
-rw-r--r--pkgs/games/gog/settlers2.nix99
-rw-r--r--pkgs/games/gog/stardew-valley.nix61
-rw-r--r--pkgs/games/gog/the-longest-journey/default.nix106
-rw-r--r--pkgs/games/gog/the-longest-journey/predefined-config.patch29
-rw-r--r--pkgs/games/gog/thimbleweed-park.nix66
-rw-r--r--pkgs/games/gog/war-for-the-overworld.nix86
-rw-r--r--pkgs/games/gog/warcraft2/default.nix148
-rw-r--r--pkgs/games/gog/warcraft2/xdg.patch37
-rw-r--r--pkgs/games/gog/wizard-of-legend.nix14
-rw-r--r--pkgs/games/gog/xeen.nix119
-rw-r--r--pkgs/games/humblebundle/antichamber.nix56
-rw-r--r--pkgs/games/humblebundle/baba-is-you.nix27
-rw-r--r--pkgs/games/humblebundle/bastion.nix98
-rw-r--r--pkgs/games/humblebundle/brigador.nix103
-rw-r--r--pkgs/games/humblebundle/cavestoryplus.nix39
-rw-r--r--pkgs/games/humblebundle/default.nix62
-rw-r--r--pkgs/games/humblebundle/dott.nix70
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/default.nix256
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch121
-rw-r--r--pkgs/games/humblebundle/fez.nix35
-rw-r--r--pkgs/games/humblebundle/ftl.nix38
-rw-r--r--pkgs/games/humblebundle/grim-fandango.nix177
-rw-r--r--pkgs/games/humblebundle/guacamelee.nix61
-rw-r--r--pkgs/games/humblebundle/hammerwatch.nix40
-rw-r--r--pkgs/games/humblebundle/jamestown.nix55
-rw-r--r--pkgs/games/humblebundle/liads.nix16
-rw-r--r--pkgs/games/humblebundle/megabytepunch.nix14
-rw-r--r--pkgs/games/humblebundle/minimetro.nix15
-rw-r--r--pkgs/games/humblebundle/opus-magnum.nix43
-rw-r--r--pkgs/games/humblebundle/owlboy.nix102
-rw-r--r--pkgs/games/humblebundle/pico-8.nix51
-rw-r--r--pkgs/games/humblebundle/rocketbirds.nix11
-rw-r--r--pkgs/games/humblebundle/spaz.nix39
-rw-r--r--pkgs/games/humblebundle/starbound.nix328
-rw-r--r--pkgs/games/humblebundle/swordsandsoldiers.nix43
-rw-r--r--pkgs/games/humblebundle/the-bridge.nix37
-rw-r--r--pkgs/games/humblebundle/trine2.nix97
-rw-r--r--pkgs/games/humblebundle/unepic.nix44
-rw-r--r--pkgs/games/itch/default.nix29
-rw-r--r--pkgs/games/itch/fetch-itch/default.nix81
-rw-r--r--pkgs/games/itch/invisigun-heroes.nix43
-rw-r--r--pkgs/games/itch/towerfall-ascension.nix90
-rw-r--r--pkgs/games/steam/default.nix38
-rw-r--r--pkgs/games/steam/fetchsteam/default.nix93
-rw-r--r--pkgs/games/steam/fetchsteam/downloader.patch34
-rw-r--r--pkgs/games/steam/starbound.nix207
-rw-r--r--pkgs/lib/call-package-scope.nix25
-rw-r--r--pkgs/list-gamecontrollers/default.nix10
-rw-r--r--pkgs/list-gamecontrollers/list-gc.c31
-rw-r--r--pkgs/openlab/default.nix7
-rw-r--r--pkgs/openlab/gitit/default.nix18
-rw-r--r--pkgs/openlab/stackenblocken/default.nix86
-rwxr-xr-xpkgs/profpatsch/backlight/backlight.py38
-rw-r--r--pkgs/profpatsch/backlight/default.nix17
-rw-r--r--pkgs/profpatsch/default.nix183
-rw-r--r--pkgs/profpatsch/display-infos/default.nix72
-rw-r--r--pkgs/profpatsch/execline/e.nix24
-rw-r--r--pkgs/profpatsch/execline/escape.nix31
-rw-r--r--pkgs/profpatsch/execline/importer.nix45
-rw-r--r--pkgs/profpatsch/execline/run-execline-tests.nix89
-rw-r--r--pkgs/profpatsch/execline/run-execline.nix71
-rw-r--r--pkgs/profpatsch/execline/runblock.nix69
-rw-r--r--pkgs/profpatsch/execline/symlink.nix46
-rw-r--r--pkgs/profpatsch/execline/write-execline.nix35
-rw-r--r--pkgs/profpatsch/git-commit-index/default.nix69
-rw-r--r--pkgs/profpatsch/git-commit-index/lib.sh102
-rw-r--r--pkgs/profpatsch/nix-http-serve/default.nix11
-rw-r--r--pkgs/profpatsch/nman/default.nix14
-rw-r--r--pkgs/profpatsch/nman/nman.go137
-rw-r--r--pkgs/profpatsch/query-album-streams/default.nix3
-rw-r--r--pkgs/profpatsch/query-album-streams/last-fm-api.nix87
-rw-r--r--pkgs/profpatsch/s6/dhall/ServiceDirectory/default-type.dhall17
-rw-r--r--pkgs/profpatsch/s6/dhall/ServiceDirectory/default.dhall20
-rw-r--r--pkgs/profpatsch/s6/dhall/ServiceDirectory/type.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/defaults.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/imports/Signal.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/imports/Signal/type.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/types.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/alpha.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/arm.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/common/alpha-sparc.dhall35
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/common/constants.dhall0
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/common/mips.dhall35
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/common/standard.dhall25
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/common/type.dhall63
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/common/x86-arm.dhall35
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/mips.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/misc/alpha-sparc.dhall14
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/misc/common-type.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/misc/mips.dhall14
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/misc/x86-arm.dhall14
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/sparc.dhall1
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/type.dhall2
-rw-r--r--pkgs/profpatsch/s6/dhall/unix/Signal/x86.dhall1
-rw-r--r--pkgs/profpatsch/sandbox.nix63
-rw-r--r--pkgs/profpatsch/sfttime/default.nix14
-rwxr-xr-xpkgs/profpatsch/sfttime/sfttime.sh145
-rw-r--r--pkgs/profpatsch/show-qr-code/default.nix28
-rw-r--r--pkgs/profpatsch/testing/default.nix92
-rw-r--r--pkgs/profpatsch/utils-hs/default.nix105
-rw-r--r--pkgs/profpatsch/warpspeed/default.nix60
-rw-r--r--pkgs/profpatsch/xmonad/DhallTypedInput.hs232
-rwxr-xr-xpkgs/profpatsch/youtube2audiopodcast/Main.hs113
-rw-r--r--pkgs/profpatsch/youtube2audiopodcast/default.nix173
-rw-r--r--pkgs/sternenseemann/default.nix6
-rw-r--r--pkgs/sternenseemann/logbook/default.nix26
-rw-r--r--pkgs/sternenseemann/spacecookie/default.nix25
-rw-r--r--pkgs/taalo-build/default.nix36
-rw-r--r--release.nix229
-rw-r--r--tests/aszlig/dnyarri/luks2-bcache.nix132
-rw-r--r--tests/aszlig/programs/psi.nix25
-rw-r--r--tests/default.nix24
-rw-r--r--tests/games/starbound.nix103
-rw-r--r--tests/make-test.nix35
-rw-r--r--tests/programs/gnupg/default.nix136
-rw-r--r--tests/programs/gnupg/snakeoil.asc59
-rw-r--r--tests/sandbox.nix140
-rw-r--r--tests/system/kernel/bfq.nix14
278 files changed, 22217 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 00000000..8377ade4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,17 @@
+All Nix expressions in this repository are free software: you can
+redistribute them and/or modify them under the terms of the GNU General
+Public License as published by the Free Software Foundation, either
+version 3 of the License, or (at your option) any later version.
+
+The expression files are distributed in the hope that they will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+Public License for more details.
+
+For files other than the Nix expression files the same terms apply,
+unless explicitly stated otherwise here or in the file header.
+
+You should have received a copy of the GNU General Public License
+along with this program (see LICENSE file).
+
+If not, see <http://www.gnu.org/licenses/>.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..37d71f5b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+About Vuizvui
+=============
+
+This contains a set of NixOS modules/configurations and various other Nix
+expressions used by [OpenLab Augsburg](https://openlab-augsburg.de) and its
+members.
+
+Documentation
+=============
+
+You can find the latest build of the documentation here:
+
+https://headcounter.org/hydra/job/openlab/vuizvui/manual/latest/download
diff --git a/default.nix b/default.nix
new file mode 100644
index 00000000..86e58376
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,13 @@
+{ system ? builtins.currentSystem, ... }@args:
+
+{
+  machines = import ./machines;
+
+  pkgs = import ./pkgs {
+    pkgs = import (import ./nixpkgs-path.nix) args;
+  };
+
+  lib = import "${import ./nixpkgs-path.nix}/lib" // {
+    vuizvui = import ./lib;
+  };
+}
diff --git a/doc/entities.ent b/doc/entities.ent
new file mode 100644
index 00000000..cee77e2d
--- /dev/null
+++ b/doc/entities.ent
@@ -0,0 +1,22 @@
+<!ENTITY nixos.url "https://nixos.org/">
+<!ENTITY nixpkgs.url "https://nixos.org/nixpkgs/">
+<!ENTITY openlab.url "https://openlab-augsburg.de/">
+
+<!ENTITY nixos '
+<link xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xlink:href="&nixos.url;">NixOS</link>
+'>
+<!ENTITY nixpkgs '
+<link xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xlink:href="&nixpkgs.url;">&lt;nixpkgs&gt;</link>
+'>
+<!ENTITY openlab '
+<link xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xlink:href="&openlab.url;">OpenLab Augsburg</link>
+'>
+
+<!ENTITY hydra.base "https://headcounter.org/hydra">
+<!ENTITY hydra.channelbase "&hydra.base;/channel/custom/openlab/vuizvui">
diff --git a/doc/index.xml b/doc/index.xml
new file mode 100644
index 00000000..efb0bf2f
--- /dev/null
+++ b/doc/index.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!DOCTYPE book [
+<!ENTITY % entities SYSTEM "entities.ent">
+%entities;
+]>
+<book xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+  <title>Vuizvui</title>
+  <preface>
+    <title>What is Vuizvui?</title>
+    <para>
+      Vuizvui is a set of NixOS modules and machine configurations that aim to
+      extend &nixos; in a way that fits the needs of the members of the
+      &openlab; and also serves as a playground for experimental features that
+      are yet to be included in the official NixOS project once they're well
+      tested and matured enough.
+    </para>
+
+    <para>
+      This means that module options in Vuizvui are subject to change without
+      retaining backwards-compatibility for configurations outside of the
+      defined machines.
+    </para>
+
+    <para>
+      The name <literal>Vuizvui</literal> is of Bavarian origins and means
+      something like <literal>too much</literal> while on the other side
+      <literal>nix</literal> means nothing. Which fits quite well because this
+      repository is for everything either too complex or not polished/generic
+      enough to be pushed into &nixpkgs;.
+    </para>
+  </preface>
+
+  <xi:include href="install.xml" />
+  <xi:include href="options.xml" />
+</book>
diff --git a/doc/install.xml b/doc/install.xml
new file mode 100644
index 00000000..88a0838f
--- /dev/null
+++ b/doc/install.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<!DOCTYPE book [
+<!ENTITY % entities SYSTEM "entities.ent">
+%entities;
+]>
+<part xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+  <title>Installing a machine in Vuizvui</title>
+
+  <para>
+    The easiest way to get started is if the machine is already in Vuizvui so
+    there is a channel available.
+  </para>
+
+  <para>
+    You can have a look at
+    <link xlink:href="&hydra.base;/jobset/openlab/vuizvui#tabs-channels">the
+      list of channels</link>
+    to check whether a channel exists for the machine you want to install.
+  </para>
+
+  <para>
+    So let's say you want to install the machine <literal>schnurrkadse</literal> which
+    has the channel attribute <literal>channels.machines.sternenseemann.schnurrkadse</literal>
+  </para>
+
+  <para>
+    First you need to add the channel for the
+    <systemitem class="username">root</systemitem> user of your current system
+    using the following commands:
+
+    <command>
+<screen>
+nix-channel --add <link
+  xlink:href="&hydra.channelbase;/channels.machines.sternenseemann.schnurrkadse"/> vuizvui
+nix-channel --remove nixos  # otherwise it will interfere with the rebuild
+nix-channel --update
+</screen>
+    </command>
+
+    Notice the <literal>vuizvui</literal> argument at the end of the first
+    command. This makes the channel available as
+    <literal>&lt;vuizvui&gt;</literal> in the search path of the current system.
+  </para>
+
+  <para>
+    For the first installation the <envar>NIX_PATH</envar> isn't correctly set
+    and will be set to include the <literal>vuizvui</literal> channel after
+    you've switched to the configuration for the first time.
+  </para>
+
+  <para>
+    Next put the following in your
+    <filename>/etc/nixos/configuration.nix</filename>:
+  </para>
+
+  <screen><code language="nix">(import &lt;vuizvui/machines&gt;).sternenseemann.schnurrkadse.config</code></screen>
+
+  <para>
+    Of course you need to replace <literal>sternenseemann.schnurrkadse</literal> with the
+    attribute of your machine.
+  </para>
+
+  <para>
+    Now in order to do the first build and activation of the configuration, you
+    need to issue the following command as root:
+  </para>
+
+  <!-- FIXME: This WON'T work because of wrong NIX_PATH and missicg binary
+              cache public key! -->
+  <!-- TODO: create a bootsrap script that does this automatically -->
+  <screen><command>nixos-rebuild \
+  -I nixpkgs=/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs \
+  --option binary-cache-public-keys "headcounter.org:/7YANMvnQnyvcVB6rgFTdb8p5LG1OTXaO+21CaOSBzg=" \
+      switch</command></screen>
+
+  <para>
+    We redefine <literal>nixpkgs</literal> here, because vuizvui brings its own nixpkgs that gets build on the hydra, using it we get to download from the binary cache. Additionally, we need to manually specify the public key for the <literal>headcounter.org</literal> hydra.
+  </para>
+</part>
diff --git a/doc/options.xml b/doc/options.xml
new file mode 100644
index 00000000..da30e6c8
--- /dev/null
+++ b/doc/options.xml
@@ -0,0 +1,9 @@
+<part xmlns="http://docbook.org/ns/docbook"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    xmlns:xi="http://www.w3.org/2001/XInclude">
+    <title>Vuizvui-specific NixOS options</title>
+    <para>
+        The following NixOS options are specific to Vuizvui:
+    </para>
+    <xi:include href="options-db.xml" />
+</part>
diff --git a/lib/call-machine.nix b/lib/call-machine.nix
new file mode 100644
index 00000000..f500dd21
--- /dev/null
+++ b/lib/call-machine.nix
@@ -0,0 +1,73 @@
+path: cfg:
+
+let
+  __withPkgsPath = nixpkgs: rec {
+    eval = import "${nixpkgs}/nixos/lib/eval-config.nix" {
+      modules = [ path cfg ] ++ import ../modules/module-list.nix;
+    };
+
+    build = eval.config.system.build.toplevel;
+
+    iso = mkIso "installer/cd-dvd/iso-image.nix" (
+      { lib, ... }: let
+        name = eval.config.networking.hostName;
+        upperName = lib.toUpper name;
+      in rec {
+        isoImage.isoName = "${name}.iso";
+        isoImage.volumeID = builtins.substring 0 11 "${upperName}_LIVE";
+        isoImage.makeEfiBootable = true;
+        isoImage.makeUsbBootable = true;
+        isoImage.appendToMenuLabel = " \"${name}\" Live System";
+      }
+    );
+
+    installerIso = mkIso "installer/cd-dvd/installation-cd-minimal.nix" {
+      environment.sessionVariables = {
+        NIX_PATH = [ "vuizvui=${../.}" ];
+      };
+    };
+
+    mkIso = isoModule: extraConfig: let
+      wrapIso = { config, pkgs, lib, ... }@attrs: let
+        isoEval = import "${nixpkgs}/nixos/modules/${isoModule}" attrs;
+        isoEvalcfg = isoEval.config or {};
+        bootcfg = isoEvalcfg.boot or {};
+        fscfg = isoEvalcfg.fileSystems or {};
+      in {
+        options = isoEval.options or {};
+        imports = (isoEval.imports or []) ++ [ extraConfig ];
+        config = isoEvalcfg // {
+          boot = bootcfg // lib.optionalAttrs (bootcfg ? loader) {
+            loader = lib.mkForce bootcfg.loader;
+          };
+          fileSystems = lib.mapAttrs (lib.const lib.mkForce) fscfg // {
+            "/boot" = lib.mkForce (fscfg."/boot" or {
+              device = "none";
+              fsType = "none";
+              options = [ "noauto" ];
+            });
+          };
+        };
+      };
+    in import "${nixpkgs}/nixos/lib/eval-config.nix" {
+      modules = [ config wrapIso ];
+    };
+
+    config = {
+      imports = [ path cfg ] ++ import ../modules/module-list.nix;
+    };
+
+    vm = (import "${nixpkgs}/nixos" {
+      configuration = config;
+    }).vm;
+  };
+
+in rec {
+  inherit (__withPkgsPath (import ../nixpkgs-path.nix))
+    build config eval iso installerIso vm;
+
+  # This is internal only and for use with restricted evaluation mode in Hydra
+  # to get the path to nixpkgs from the jobset input args instead of
+  # ../nixpkgs-path.nix.
+  inherit __withPkgsPath;
+}
diff --git a/lib/call-network.nix b/lib/call-network.nix
new file mode 100644
index 00000000..f65b1156
--- /dev/null
+++ b/lib/call-network.nix
@@ -0,0 +1,18 @@
+path: args:
+
+let
+  machineAttrs = import path;
+  machineNames = builtins.attrNames machineAttrs;
+
+  mkMachine = name: {
+    inherit name;
+    value = import ./call-machine.nix machineAttrs.${name} ({ lib, ... }: {
+      imports = lib.singleton (args.extraConfig or {});
+      networking.hostName = lib.mkOverride 900 name;
+      _module.args.nodes = lib.mapAttrs (lib.const (m: m ? eval)) machines;
+    } // removeAttrs args [ "extraConfig" ]);
+  };
+
+  machines = builtins.listToAttrs (map mkMachine machineNames);
+
+in machines
diff --git a/lib/default.nix b/lib/default.nix
new file mode 100644
index 00000000..05118275
--- /dev/null
+++ b/lib/default.nix
@@ -0,0 +1,5 @@
+rec {
+  callMachine = import ./call-machine.nix;
+  callNetwork = import ./call-network.nix;
+  getVuizvuiTests = import ./get-tests.nix;
+}
diff --git a/lib/get-tests.nix b/lib/get-tests.nix
new file mode 100644
index 00000000..f14ee3cc
--- /dev/null
+++ b/lib/get-tests.nix
@@ -0,0 +1,24 @@
+{ system ? builtins.currentSystem
+, nixpkgs ? import ../nixpkgs-path.nix
+, vuizvuiTests ? ../tests
+, excludeVuizvuiGames ? false
+}:
+
+with import "${nixpkgs}/lib";
+
+{
+  nixos = let
+    upstreamTests = (import "${nixpkgs}/nixos/release.nix" {
+      inherit nixpkgs;
+    }).tests;
+    isTestOrJob = attr: (attr.type or null) == "derivation" || attr ? test;
+    isTestOrSystems = attr: isTestOrJob attr || attr ? ${system};
+    cond = attr: !isTestOrSystems attr;
+    reduce = attr: if isTestOrJob attr then attr else attr.${system};
+  in mapAttrsRecursiveCond cond (path: reduce) upstreamTests;
+
+  vuizvui = removeAttrs (import vuizvuiTests {
+    inherit system;
+    nixpkgsPath = nixpkgs;
+  }) (optional excludeVuizvuiGames "games");
+}
diff --git a/machines/README.md b/machines/README.md
new file mode 100644
index 00000000..9d3fe3af
--- /dev/null
+++ b/machines/README.md
@@ -0,0 +1,4 @@
+This directory contains NixOS machine configurations.
+
+Feel free to add your own configuration into a subdirectory named after your
+nickname or handle and update default.nix accordingly.
diff --git a/machines/aszlig/dnyarri.nix b/machines/aszlig/dnyarri.nix
new file mode 100644
index 00000000..3d9ae86a
--- /dev/null
+++ b/machines/aszlig/dnyarri.nix
@@ -0,0 +1,139 @@
+{ config, pkgs, utils, lib, ... }:
+
+let
+  mkDevice = category: num: uuid: {
+    name = "dnyarri-${category}-crypt-${toString num}";
+    value.device = "/dev/disk/by-uuid/${uuid}";
+  };
+
+  cryptDevices = {
+    root = lib.imap (mkDevice "root") [
+      "b13d257e-b5fd-4f86-82b1-8bfe06335a75"
+      "a607c827-2fd7-49d9-a7d8-05279c8653a4"
+      "de32cb42-2e09-4e6a-84b4-244078d289c8"
+      "12dac5b2-7647-45de-b752-5efee23855d0"
+    ];
+    swap = lib.imap (mkDevice "swap") [
+      "e0a8281d-2c68-48ca-8e00-f0defaf51f38"
+      "d26e61d6-c238-4c01-8c57-b1ba0bdb8c93"
+    ];
+  };
+
+  bcacheMode = "writearound";
+
+  bcacheStart = ''
+    for i in /sys/block/bcache[0-9]*/bcache/cache_mode; do
+      echo ${lib.escapeShellArg bcacheMode} > "$i"
+    done
+  '';
+
+  bcacheStop = ''
+    for i in /sys/block/bcache[0-9]*/bcache/cache_mode; do
+      echo none > "$i"
+    done
+  '';
+
+in {
+  vuizvui.user.aszlig.profiles.workstation.enable = true;
+
+  vuizvui.requiresTests = [
+    ["vuizvui" "aszlig" "dnyarri" "luks2-bcache"]
+  ];
+
+  nix.maxJobs = 8;
+
+  boot = {
+    loader.systemd-boot.enable = true;
+    loader.grub.enable = lib.mkForce false;
+    loader.efi.canTouchEfiVariables = true;
+
+    kernelPackages = pkgs.linuxPackages_latest;
+
+    initrd = {
+      availableKernelModules = [ "bcache" ];
+      luks.devices =
+        lib.listToAttrs (lib.concatLists (lib.attrValues cryptDevices));
+    };
+  };
+
+  environment.systemPackages = [
+    pkgs.gpodder
+    pkgs.paperwork
+  ];
+
+  # This is very ugly and I really want to avoid non-free packages on all
+  # of my workstations. But right now I need to get rid of useless paper.
+  nixpkgs.config.allowUnfreePredicate = pkg: let
+    inherit (builtins.parseDrvName (pkg.name or "")) name;
+  in name == "hplip";
+  nixpkgs.overlays = lib.singleton (lib.const (super: {
+    hplip = super.hplip.override { withPlugin = true; };
+  }));
+
+  hardware.sane.enable = true;
+  hardware.sane.extraBackends = [ pkgs.hplip ];
+
+  vuizvui.system.kernel.bfq.enable = true;
+  hardware.enableRedistributableFirmware = true;
+
+  networking.hostName = "dnyarri";
+  networking.interfaces.eno0.useDHCP = true;
+
+  fileSystems = {
+    "/boot" = {
+      device = "/dev/disk/by-uuid/9A75-9A6E";
+      fsType = "vfat";
+    };
+    "/" = {
+      label = "dnyarri-root";
+      fsType = "btrfs";
+      options = [ "autodefrag" "space_cache" "compress=zstd" "noatime" ];
+    };
+  };
+
+  powerManagement.powerUpCommands = ''
+    ${pkgs.hdparm}/sbin/hdparm -B 255 /dev/disk/by-id/ata-ST31500541AS_5XW0AMNH
+    ${pkgs.hdparm}/sbin/hdparm -B 255 /dev/disk/by-id/ata-ST31500541AS_6XW0M217
+    ${bcacheStart}
+  '';
+
+  powerManagement.powerDownCommands = bcacheStop;
+
+  services.btrfs.autoScrub.enable = true;
+
+  # Inject preStart/postStart for activating/deactivating bcache to the scrub
+  # services, so we don't get large amounts of nonsense on the caching device.
+  systemd.services = let
+    scrubServiceUnits = let
+      mkName = fs: "btrfs-scrub-${utils.escapeSystemdPath fs}.service";
+    in map mkName config.services.btrfs.autoScrub.fileSystems;
+  in lib.genAttrs scrubServiceUnits (lib.const {
+    preStart = bcacheStop;
+    postStart = bcacheStart;
+  });
+
+  swapDevices = map ({ name, ... }: {
+    device = "/dev/mapper/${name}";
+  }) cryptDevices.swap;
+
+  users.users.aszlig.extraGroups = [
+    "scanner"
+    # TODO: Try to avoid this, but as there is only a single user using audio
+    # on this machine, it's okay for now. But remember that this will break
+    # heavily, should there be another user accessing the audio devices.
+    "audio"
+  ];
+
+  services.xserver.videoDrivers = [ "ati" ];
+  services.xserver.xrandrHeads = [ "DVI-0" "HDMI-0" ];
+
+  vuizvui.user.aszlig.services.i3.workspaces."1" = {
+    label = "XMPP";
+    assign = lib.singleton { class = "^(?:Tkabber|Gajim)\$"; };
+  };
+
+  vuizvui.user.aszlig.services.i3.workspaces."3" = {
+    label = "Browser";
+    assign = lib.singleton { class = "^Firefox\$"; };
+  };
+}
diff --git a/machines/aszlig/managed/brawndo.nix b/machines/aszlig/managed/brawndo.nix
new file mode 100644
index 00000000..8fb96ec8
--- /dev/null
+++ b/machines/aszlig/managed/brawndo.nix
@@ -0,0 +1,54 @@
+{ config, pkgs, unfreePkgs, unfreeAndNonDistributablePkgs, lib, ... }:
+
+let
+  mainDisk = "ata-WDC_WD5000LPVX-22V0TT0_WD-WXG1E2559AYH";
+  rootUUID = "dbbd5a35-3ac0-4d5a-837d-914457de14a4";
+
+in {
+  boot = {
+    initrd.availableKernelModules = [
+      "xhci_pci" "ehci_pci" "ahci" "usb_storage" "sd_mod" "sr_mod"
+      "rtsx_pci_sdmmc"
+    ];
+    kernelModules = [ "kvm-intel" "wl" ];
+    kernelPackages = pkgs.linuxPackages_latest;
+    extraModulePackages = [ config.boot.kernelPackages.broadcom_sta ];
+  };
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-uuid/${rootUUID}";
+    fsType = "btrfs";
+    options = [ "compress=zstd" "space_cache" "noatime" ];
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-uuid/534F-980B";
+    fsType = "vfat";
+  };
+
+  hardware.enableAllFirmware = true;
+  hardware.cpu.intel.updateMicrocode = true;
+
+  networking.hostName = "brawndo";
+
+  nix.maxJobs = 4;
+
+  nixpkgs.config.allowUnfree = true; # XXX: More granularity!
+
+  environment.systemPackages = with pkgs; [
+    vuizvui.aszlig.axbo chromium firefox gpodder opentyrian unfreePkgs.steam
+    python3 unfreeAndNonDistributablePkgs.vscode-with-extensions
+  ];
+
+  i18n.defaultLocale = "en_US.UTF-8";
+
+  services = {
+    deluge.enable = true;
+    printing.drivers = [ pkgs.cups-bjnp pkgs.cnijfilter2 ];
+  };
+
+  swapDevices = lib.singleton { label = "swap"; };
+
+  vuizvui.user.aszlig.profiles.managed.enable = true;
+  vuizvui.user.aszlig.profiles.managed.mainUser = "dwenola";
+}
diff --git a/machines/aszlig/managed/shakti.nix b/machines/aszlig/managed/shakti.nix
new file mode 100644
index 00000000..8440ce6f
--- /dev/null
+++ b/machines/aszlig/managed/shakti.nix
@@ -0,0 +1,72 @@
+{ pkgs, unfreeAndNonDistributablePkgs, lib, ... }:
+
+{
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  boot.initrd.luks.devices = [
+    { name = "00vault";
+      device = "/dev/disk/by-uuid/a70f4ff8-e463-42fa-8148-6783dd352f96";
+    }
+    { name = "shakti-swap";
+      device = "/dev/disk/by-uuid/69f3a774-c796-4dbd-a38b-32f019d05e7c";
+      keyFile = "/dev/mapper/00vault";
+    }
+    { name = "shakti-root";
+      device = "/dev/disk/by-uuid/8a67bdf9-08bb-4214-b728-88cf1c2ee206";
+      keyFile = "/dev/mapper/00vault";
+    }
+  ];
+  boot.initrd.postDeviceCommands = lib.mkAfter ''
+    cryptsetup luksClose /dev/mapper/00vault
+  '';
+
+  boot.kernelModules = [ "kvm-amd" ];
+
+  # The machine has a weird HDMI->DVI adapter which doesn't report back EDID
+  # information and the monitor is also very weird because it doesn't
+  # understand the fallback modes.
+  boot.kernelParams = [ "drm_kms_helper.edid_firmware=edid/weird.bin" ];
+  hardware.firmware = lib.singleton (pkgs.runCommandLocal "weird-edid" {} ''
+    mkdir -p "$out/lib/firmware/edid"
+    base64 -d > "$out/lib/firmware/edid/weird.bin" <<EOF
+    AP///////wAEaaEiAQEBASQRAQOALx1477U1pVZKmiUQUFS/74BxT4GAlQCzAAEBlQ+BioFA
+    ITmQMGIaJ0BosDYAsQ4RAAAcAAAA/QA3Sx5QEQIAIFBYAoAoAAAA/wA3OUxNTVMwMDAyMDAK
+    AAAA/ABBU1VTIFBHMjIxCiAgAc8CAxABSwIRBBMFFBAfCQAAjArQiiDgLRAQPpYAE44hAAAY
+    AR0AclHQHiBuKFUAxI4hAAAYAR2AGHEcFiBYLCUAxI4hAACe8zmAGHE4LUBYLEUAxI4hAAAe
+    jArQkCBAMSAMQFUAE44hAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAASw==
+    EOF
+  '');
+
+  environment.systemPackages = with pkgs; [
+    mosh wget krita gphoto2 digikam unfreeAndNonDistributablePkgs.dropbox
+    firefox
+  ];
+
+  fileSystems."/boot".device = "/dev/disk/by-uuid/D54F-2AF3";
+  fileSystems."/boot".fsType = "vfat";
+
+  fileSystems."/".device = "/dev/mapper/shakti-root";
+  fileSystems."/".fsType = "btrfs";
+  fileSystems."/".options = [
+    "compress=zstd"
+    "noatime"
+    "space_cache"
+  ];
+
+  swapDevices = lib.singleton {
+    device = "/dev/mapper/shakti-swap";
+  };
+
+  networking.hostName = "shakti";
+
+  hardware.cpu.amd.updateMicrocode = true;
+
+  nix.maxJobs = 4;
+
+  services.xserver.xkbOptions = "eurosign:e,caps:none";
+
+  services.deluge.enable = true;
+
+  vuizvui.user.aszlig.profiles.managed.enable = true;
+  vuizvui.user.aszlig.profiles.managed.mainUser = "aortab";
+}
diff --git a/machines/aszlig/managed/tyree.nix b/machines/aszlig/managed/tyree.nix
new file mode 100644
index 00000000..9296d773
--- /dev/null
+++ b/machines/aszlig/managed/tyree.nix
@@ -0,0 +1,72 @@
+{ pkgs, lib, ... }:
+
+{
+  boot.initrd.availableKernelModules = [ "usbhid" ];
+  boot.kernelModules = [ "kvm-intel" ];
+
+  boot.initrd.luks.devices = [
+    { name = "00-vault";
+      device = "/dev/disk/by-uuid/e4eb3d30-7fa5-4af4-86fb-80b47518cc25";
+    }
+    { name = "tyree-swap";
+      device = "/dev/disk/by-uuid/d96e29b4-0b9a-442d-af27-805f69ffffb3";
+      keyFile = "/dev/mapper/00-vault";
+    }
+    { name = "tyree-root";
+      device = "/dev/disk/by-uuid/21e9a86e-c8dc-4d8f-ba75-d03552dc32f7";
+      keyFile = "/dev/mapper/00-vault";
+    }
+  ];
+
+  boot.initrd.postDeviceCommands = lib.mkAfter ''
+    cryptsetup luksClose /dev/mapper/00-vault
+  '';
+
+  environment.systemPackages = with pkgs; [
+    aqbanking darktable digikam firefox gwenhywfar gphoto2 kgpg kmymoney krita
+    mosh python2Packages.weboob rawtherapee wget
+  ];
+
+  fileSystems."/boot".device = "/dev/disk/by-uuid/A0D5-269D";
+  fileSystems."/boot".fsType = "vfat";
+
+  fileSystems."/".label = "tyree-root";
+  fileSystems."/".fsType = "btrfs";
+  fileSystems."/".options = [
+    "compress=zstd"
+    "discard"
+    "noatime"
+    "space_cache"
+    "ssd"
+  ];
+
+  swapDevices = lib.singleton {
+    label = "tyree-swap";
+  };
+
+  i18n.defaultLocale = "de_DE.UTF-8";
+
+  networking.hostName = "tyree";
+  networking.useNetworkd = true;
+  networking.useDHCP = false;
+  networking.interfaces.wlan0.useDHCP = true;
+
+  hardware.cpu.intel.updateMicrocode = true;
+
+  nix.maxJobs = 4;
+
+  # English within the shell, German otherwise (like in KDE).
+  programs.bash.interactiveShellInit = lib.mkBefore ''
+    export LANG=en_US.UTF-8
+  '';
+
+  services.journald.extraConfig = "SystemMaxUse=100M";
+
+  services.xserver.xkbOptions = "eurosign:e,caps:none";
+  services.xserver.wacom.enable = true;
+
+  vuizvui.user.aszlig.profiles.managed.enable = true;
+  vuizvui.user.aszlig.profiles.managed.mainUser = "bla";
+
+  vuizvui.hardware.t100ha.enable = true;
+}
diff --git a/machines/aszlig/meshuggah.nix b/machines/aszlig/meshuggah.nix
new file mode 100644
index 00000000..4a98022d
--- /dev/null
+++ b/machines/aszlig/meshuggah.nix
@@ -0,0 +1,56 @@
+{ pkgs, lib, ... }:
+
+{
+  vuizvui.user.aszlig.profiles.base.enable = true;
+  vuizvui.user.aszlig.programs.zsh.machineColor = "cyan";
+
+  boot.initrd.availableKernelModules = [ "sdhci_acpi" ];
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+  boot.loader.grub.enable = lib.mkForce false;
+  boot.loader.efi.canTouchEfiVariables = true;
+  boot.loader.systemd-boot.enable = true;
+
+  networking.hostName = "meshuggah";
+
+  nix.maxJobs = 2;
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-uuid/9bddc8d3-88ee-4aac-b885-c9abca36b863";
+    fsType = "btrfs";
+    options = [
+      "compress=lzo"
+      "discard"
+      "noatime"
+      "space_cache"
+      "ssd"
+    ];
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-uuid/A318-8495";
+    fsType = "vfat";
+  };
+
+  services.openssh.enable = true;
+  services.openssh.permitRootLogin = "without-password";
+
+  swapDevices = lib.singleton {
+    device = "/dev/disk/by-uuid/2738fbe5-fee8-4835-a512-241f447252fa";
+  };
+
+  users.users.root.openssh.authorizedKeys.keys = let
+    mkKey = name: type: data: "${type} ${lib.concatStrings data} ${name}";
+  in lib.singleton (mkKey "openpgp:0x6321DF96" "ssh-rsa" [
+    "AAAAB3NzaC1yc2EAAAADAQABAAACAQCWc5omkAV4yV9gn11kHPlxfSXHlIROkJZAmn"
+    "towEMeUAyOI38gc3QNCTYRpo8bpD68U4X/p0NHIetm9v4t9t8Nsz/Tj3KHmh291DIj"
+    "W4IisrjCX1o9aj5ESu2bCNp+6oEWjb2GbecJjn3kf4eh82imh4F9s0PtGzCUB+iYgP"
+    "OiMCj4pfMK76yu6SKoyU43FwhkD+v5DgmBT0+GPftce4Dyrh3GV8qUotP32hohsFq3"
+    "aWxcRU6Y3yRwRiUskh1B6+H3W44peX+F+0j6jnX49DwAwXfFVAmqDqxLz4r37uRjYe"
+    "G+6UGt6fm2hgYyLf07ph8c6fQCOmYaPs7lvpBDB+zuadxAlk/bKHKgfr4xbXyoIjAV"
+    "L7uw3ui0NqbAMoBGEgi+Sk6t7JYQsyhvshauw3TyTi3Jjr1NBjHmbCNgguNLtWoFsJ"
+    "dd8Jgd1AVQbxnsLitWxSrTem9mlBRYWV+SxyBR5kqhrw36/yXVeWp+jX23Kg98Bco7"
+    "NA+QPWKXKE4HffAZQ3MJGEZJkS+9l1wCjrhAa5jjbH4Auh+bRNkKImXg26OdU7fU1O"
+    "Lgzi3J1vmcLV/HOqp66i8/biehsYL9GL2uHaqZS9Xaj177bTqveaVRjyomiNCkm0ed"
+    "+NPSboEuGyEE59D8w7FBsk/d3r3z3fZg1j++UvSwVkqXYByl5pzCnw=="
+  ]);
+}
diff --git a/machines/aszlig/tishtushi.nix b/machines/aszlig/tishtushi.nix
new file mode 100644
index 00000000..f38f894a
--- /dev/null
+++ b/machines/aszlig/tishtushi.nix
@@ -0,0 +1,63 @@
+{ pkgs, lib, ... }:
+
+{
+  vuizvui.user.aszlig.profiles.workstation.enable = true;
+
+  vuizvui.system.kernel.bfq.enable = true;
+
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+
+  boot.loader = {
+    grub.device = "/dev/disk/by-id/ata-Hitachi_HTS543232A7A384_E2P31243FGB6PJ";
+    timeout = 1;
+  };
+
+  boot.initrd = {
+    luks.devices = [
+      { name = "00vault";
+        device = "/dev/disk/by-uuid/812f19f1-9096-4367-b2e4-0c9537c52a67";
+      }
+      { name = "tishtushi-swap";
+        device = "/dev/disk/by-uuid/2934df87-5fda-4b2e-9f3b-c4c96f571407";
+        keyFile = "/dev/mapper/00vault";
+      }
+      { name = "tishtushi-root";
+        device = "/dev/disk/by-uuid/cf65f144-9205-40a5-a239-b660695a6740";
+        keyFile = "/dev/mapper/00vault";
+      }
+    ];
+    postDeviceCommands = lib.mkAfter ''
+      cryptsetup luksClose /dev/mapper/00vault
+    '';
+  };
+
+  networking.hostName = "tishtushi";
+  networking.wireless.enable = lib.mkForce true;
+  networking.interfaces.wlp2s0.useDHCP = true;
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-uuid/763a7117-3dbf-4e80-9e63-c7039477ef3d";
+    fsType = "ext4";
+  };
+
+  fileSystems."/" = {
+    device = "/dev/mapper/tishtushi-root";
+    fsType = "btrfs";
+    options = [ "space_cache" "compress=zstd" "noatime" ];
+  };
+
+  swapDevices = lib.singleton {
+    device = "/dev/mapper/tishtushi-swap";
+  };
+
+  services.tlp.enable = true;
+
+  services.xserver.videoDrivers = [ "intel" ];
+  services.xserver.synaptics.enable = true;
+  services.xserver.synaptics.tapButtons = true;
+  services.xserver.synaptics.twoFingerScroll = true;
+  services.xserver.synaptics.vertEdgeScroll = false;
+  services.xserver.synaptics.accelFactor = "0.1";
+
+  nix.maxJobs = 4;
+}
diff --git a/machines/default.nix b/machines/default.nix
new file mode 100644
index 00000000..9c7abb9c
--- /dev/null
+++ b/machines/default.nix
@@ -0,0 +1,28 @@
+with import ../lib;
+
+{
+  aszlig = {
+    dnyarri   = callMachine ./aszlig/dnyarri.nix {};
+    meshuggah = callMachine ./aszlig/meshuggah.nix {};
+    tishtushi = callMachine ./aszlig/tishtushi.nix {};
+    managed = {
+      brawndo = callMachine ./aszlig/managed/brawndo.nix {};
+      shakti  = callMachine ./aszlig/managed/shakti.nix {};
+      tyree   = callMachine ./aszlig/managed/tyree.nix {};
+    };
+  };
+  devhell = {
+    eir       = callMachine devhell/eir.nix {};
+    sigrun = callMachine devhell/sigrun.nix {};
+    hildr      = callMachine devhell/hildr.nix {};
+    gunnr      = callMachine devhell/gunnr.nix {};
+  };
+  profpatsch = {
+    shiki = callMachine ./profpatsch/shiki.nix {};
+    haku   = callMachine ./profpatsch/haku.nix {};
+    mikiya = callMachine ./profpatsch/mikiya.nix {};
+  };
+  misc = {
+    mailserver = callMachine ./misc/mailserver.nix {};
+  };
+}
diff --git a/machines/devhell/eir.nix b/machines/devhell/eir.nix
new file mode 100644
index 00000000..de5b6003
--- /dev/null
+++ b/machines/devhell/eir.nix
@@ -0,0 +1,254 @@
+{ config, pkgs, lib, ... }:
+
+{
+  vuizvui.user.devhell.profiles.base.enable = true;
+  vuizvui.system.kernel.bfq.enable = true;
+
+  boot = {
+    loader = {
+      timeout = 2;
+      systemd-boot = {
+        enable = true;
+      };
+
+      efi.canTouchEfiVariables = true;
+    };
+
+    initrd = {
+      availableKernelModules = [ "ehci_pci" "ahci" "usb_storage" ];
+      kernelModules = [ "fuse" ];
+      postDeviceCommands = ''
+        echo noop > /sys/block/sda/queue/scheduler
+      '';
+    };
+
+    kernelModules = [ "tp_smapi" ];
+    extraModulePackages = [ config.boot.kernelPackages.tp_smapi ];
+  };
+
+  hardware = {
+    cpu.intel.updateMicrocode = true;
+    opengl = {
+      enable = true;
+      extraPackages = [ pkgs.libvdpau-va-gl pkgs.vaapiVdpau pkgs.vaapiIntel ];
+    };
+  };
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-uuid/4788e218-db0f-4fd6-916e-e0c484906eb0";
+    fsType = "btrfs";
+    options = [
+      "autodefrag"
+      "space_cache"
+      "compress=zstd"
+      "noatime"
+    ];
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-uuid/BDBC-FC8B";
+    fsType = "vfat";
+  };
+
+  swapDevices = [ ];
+
+  # FIXME Check if this is still necessary in the future
+  systemd.services.systemd-networkd-wait-online.enable = false;
+
+  # XXX Ensure that these are added in addition to the DHCP provided DNS servers
+  systemd.network.networks."99-main".dns = [ "1.1.1.1" "1.0.0.1" ];
+
+  networking = {
+    hostName = "eir";
+    wireless.iwd.enable = true;
+    useNetworkd = true;
+  };
+
+  virtualisation.docker.enable = false;
+
+  nix = {
+    maxJobs = lib.mkDefault 4;
+    extraOptions = ''
+      auto-optimise-store = true
+    '';
+  };
+
+  i18n = {
+    defaultLocale = "en_GB.UTF-8";
+  };
+
+  console = {
+    font = "Lat2-Terminus16";
+    keyMap = "uk";
+  };
+
+  #### Machine-specific service configuration ####
+
+  vuizvui.user.devhell.profiles.services.enable = true;
+
+  services = {
+    thermald.enable = true;
+    tftpd.enable = false;
+    gnome3.gnome-keyring.enable = true;
+    printing = {
+      enable = true;
+      drivers = [ pkgs.foo2zjs pkgs.hplip pkgs.cups-brother-hl1110 ];
+    };
+    offlineimap = {
+      enable = true;
+      install = true;
+      path = [ pkgs.notmuch ];
+    };
+    syncthing = {
+      enable = true;
+      user = "dev";
+      dataDir = "/home/dev/syncthing/";
+    };
+  };
+
+  services.udev = {
+    extraRules = ''
+      SUBSYSTEM=="firmware", ACTION=="add", ATTR{loading}="-1"
+    '';
+  };
+
+  services.acpid = {
+    enable = true;
+    lidEventCommands = ''
+      LID="/proc/acpi/button/lid/LID/state"
+      state=`cat $LID | ${pkgs.gawk}/bin/awk '{print $2}'`
+      case "$state" in
+        *open*) ;;
+        *close*) systemctl suspend ;;
+        *) logger -t lid-handler "Failed to detect lid state ($state)" ;;
+      esac
+    '';
+  };
+
+  services.xserver = {
+    enable = true;
+    layout = "gb";
+    videoDrivers = [ "modesetting" ];
+
+    libinput = {
+      enable = true;
+      disableWhileTyping = true;
+      middleEmulation = true;
+    };
+#    synaptics = {
+#      enable = true;
+#      twoFingerScroll = true;
+#      palmDetect = true;
+#    };
+
+    # XXX: Factor out and make DRY, because a lot of the stuff here is
+    # duplicated in the other machine configurations.
+    displayManager.sessionCommands = ''
+      ${pkgs.nitrogen}/bin/nitrogen --restore &
+      ${pkgs.rofi}/bin/rofi &
+      ${pkgs.xorg.xrdb}/bin/xrdb "${pkgs.writeText "xrdb.conf" ''
+        Xft.dpi:                     96
+        Xft.antialias:               true
+        Xft.hinting:                 full
+        Xft.hintstyle:               hintslight
+        Xft.rgba:                    rgb
+        Xft.lcdfilter:               lcddefault
+        Xft.autohint:                1
+        Xcursor.theme:               Vanilla-DMZ-AA
+        Xcursor.size:                22
+        *.charClass:33:48,35:48,37:48,43:48,45-47:48,61:48,63:48,64:48,95:48,126:48,35:48,58:48
+        *background:                 #121212
+        *foreground:                 #babdb6
+        ${lib.concatMapStrings (xterm: ''
+            ${xterm}.termName:       xterm-256color
+            ${xterm}*bellIsUrgent:   true
+            ${xterm}*utf8:           1
+            ${xterm}*locale:             true
+            ${xterm}*utf8Title:          true
+            ${xterm}*utf8Fonts:          1
+            ${xterm}*utf8Latin1:         true
+            ${xterm}*dynamicColors:      true
+            ${xterm}*eightBitInput:      true
+            ${xterm}*faceName:           xft:DejaVu Sans Mono for Powerline:pixelsize=9:antialias=true:hinting=true
+            ${xterm}*faceNameDoublesize: xft:Unifont:pixelsize=12:antialias=true:hinting=true
+            ${xterm}*cursorColor:        #545f65
+        '') [ "UXTerm" "XTerm" ]}
+      ''}"
+    '';
+  };
+
+  services.tlp = {
+    enable = true;
+    extraConfig = ''
+      TLP_ENABLE = 1
+      DISK_IDLE_SECS_ON_BAT=2
+      MAX_LOST_WORK_SECS_ON_AC=15
+      MAX_LOST_WORK_SECS_ON_BAT=60
+      SCHED_POWERSAVE_ON_AC=0
+      SCHED_POWERSAVE_ON_BAT=1
+      NMI_WATCHDOG=0
+      DISK_DEVICES="sda sdb"
+      DISK_APM_LEVEL_ON_AC="254 254"
+      DISK_APM_LEVEL_ON_BAT="254 127"
+      DISK_IOSCHED="bfq bfq"
+      SATA_LINKPWR_ON_AC=max_performance
+      SATA_LINKPWR_ON_BAT=min_power
+      PCIE_ASPM_ON_AC=performance
+      PCIE_ASPM_ON_BAT=powersave
+      WIFI_PWR_ON_AC=1
+      WIFI_PWR_ON_BAT=5
+      WOL_DISABLE=Y
+      SOUND_POWER_SAVE_ON_AC=0
+      SOUND_POWER_SAVE_ON_BAT=1
+      SOUND_POWER_SAVE_CONTROLLER=Y
+      RUNTIME_PM_ON_AC=on
+      RUNTIME_PM_ON_BAT=auto
+      RUNTIME_PM_ALL=1
+      USB_AUTOSUSPEND=1
+      USB_BLACKLIST_WWAN=1
+      RESTORE_DEVICE_STATE_ON_STARTUP=0
+      DEVICES_TO_DISABLE_ON_STARTUP="bluetooth wwan"
+      DEVICES_TO_ENABLE_ON_STARTUP="wifi"
+      DEVICES_TO_DISABLE_ON_SHUTDOWN="bluetooth wifi wwan"
+      #DEVICES_TO_ENABLE_ON_SHUTDOWN=""
+      START_CHARGE_THRESH_BAT0=70
+      STOP_CHARGE_THRESH_BAT0=95
+      #DEVICES_TO_DISABLE_ON_LAN_CONNECT="wifi wwan"
+      #DEVICES_TO_DISABLE_ON_WIFI_CONNECT="wwan"
+      #DEVICES_TO_DISABLE_ON_WWAN_CONNECT="wifi"
+      #DEVICES_TO_ENABLE_ON_LAN_DISCONNECT="wifi wwan"
+      #DEVICES_TO_ENABLE_ON_WIFI_DISCONNECT=""
+      #DEVICES_TO_ENABLE_ON_WWAN_DISCONNECT=""
+    '';
+  };
+
+  #### Machine-specific packages configuration ####
+
+  vuizvui.user.devhell.profiles.packages.enable = true;
+
+  nixpkgs.config.mpv.vaapiSupport = true;
+
+  programs.light.enable = true;
+
+  environment.systemPackages = with pkgs; [
+    aircrackng
+    cdrtools
+    dvdplusrwtools
+    glxinfo
+    horst
+    iw
+    kismet
+    libva
+    libvdpau-va-gl
+    minicom
+    pmtools
+    pmutils
+    reaverwps
+    signal-desktop
+    snort
+    vaapiVdpau
+    vdpauinfo
+    wavemon
+    xbindkeys
+  ];
+}
diff --git a/machines/devhell/gunnr.nix b/machines/devhell/gunnr.nix
new file mode 100644
index 00000000..149c48e6
--- /dev/null
+++ b/machines/devhell/gunnr.nix
@@ -0,0 +1,158 @@
+{ config, pkgs, lib, ... }:
+
+{
+  vuizvui.user.devhell.profiles.base.enable = true;
+  vuizvui.system.kernel.bfq.enable = true;
+
+  boot = {
+    loader = {
+      grub  = {
+        enable = true;
+        version = 2;
+        copyKernels = true;
+        devices = [ "/dev/sda" "/dev/sdb" ];
+      };
+    };
+
+    zfs = {
+      enableUnstable = true;
+      requestEncryptionCredentials = true;
+    };
+
+    initrd = {
+      availableKernelModules = [ "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
+      kernelModules = [ "fuse" ];
+    };
+
+    kernelParams = [ "pcie_aspm=off" ];
+    kernelModules = [ "kvm-amd" ];
+    extraModulePackages = [ ];
+    blacklistedKernelModules = [ ];
+  };
+
+  hardware = {
+    cpu.amd.updateMicrocode = true;
+    opengl = {
+      enable = true;
+      extraPackages = [ pkgs.libvdpau-va-gl pkgs.vaapiVdpau ];
+    };
+  };
+
+  fileSystems."/" = {
+    device = "zpool/root/nixos";
+    fsType = "zfs";
+  };
+
+  fileSystems."/home" = {
+    device = "zpool/home";
+    fsType = "zfs";
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-label/boot";
+    fsType = "ext4";
+  };
+
+  zramSwap.enable = true;
+
+  # FIXME Check if this is still necessary in the future
+  systemd.services.systemd-networkd-wait-online.enable = false;
+  
+  networking = {
+    hostName = "gunnr";
+    hostId = "29e6affc";
+    wireless.enable = false;
+    useNetworkd = true;
+    proxy = {
+      default = "http://wproxy.canterbury.ac.uk:3128/";
+      noProxy = "127.0.0.1,localhost";
+    };
+    interfaces.enp4s0.useDHCP = true;
+  };
+
+  nix = {
+    maxJobs = lib.mkDefault 16;
+    extraOptions = ''
+      auto-optimise-store = true
+    '';
+  };
+
+  i18n = {
+    defaultLocale = "en_US.UTF-8";
+  };
+
+  console = {
+    font = "Lat2-Terminus16";
+    keyMap = "dvorak";
+  };
+
+  #### Machine-specific service configuration ####
+
+  vuizvui.user.devhell.profiles.services.enable = true;
+
+  services.zfs.autoScrub.enable = true;
+
+  services.xserver = {
+    enable = true;
+    layout = "dvorak";
+    videoDrivers = [ "modesetting" ];
+
+    # XXX: Factor out and make DRY, because a lot of the stuff here is
+    # duplicated in the other machine configurations.
+    displayManager.sessionCommands = ''
+      ${pkgs.xbindkeys}/bin/xbindkeys &
+      ${pkgs.nitrogen}/bin/nitrogen --restore &
+      ${pkgs.xscreensaver}/bin/xscreensaver -no-splash &
+      ${pkgs.rofi}/bin/rofi &
+      ${pkgs.xorg.xrdb}/bin/xrdb "${pkgs.writeText "xrdb.conf" ''
+        Xft.dpi:                     96
+        Xft.antialias:               true
+        Xft.hinting:                 full
+        Xft.hintstyle:               hintslight
+        Xft.rgba:                    rgb
+        Xft.lcdfilter:               lcddefault
+        Xft.autohint:                1
+        Xcursor.theme:               Vanilla-DMZ-AA
+        Xcursor.size:                22
+        *.charClass:33:48,35:48,37:48,43:48,45-47:48,61:48,63:48,64:48,95:48,126:48,35:48,58:48
+        *background:                 #121212
+        *foreground:                 #babdb6
+        ${lib.concatMapStrings (xterm: ''
+            ${xterm}.termName:       xterm-256color
+            ${xterm}*bellIsUrgent:   true
+            ${xterm}*utf8:           1
+            ${xterm}*locale:             true
+            ${xterm}*utf8Title:          true
+            ${xterm}*utf8Fonts:          1
+            ${xterm}*utf8Latin1:         true
+            ${xterm}*dynamicColors:      true
+            ${xterm}*eightBitInput:      true
+            ${xterm}*faceName:           xft:DejaVu Sans Mono for Powerline:pixelsize=9:antialias=true:hinting=true
+            ${xterm}*faceNameDoublesize: xft:Unifont:pixelsize=12:antialias=true:hinting=true
+            ${xterm}*cursorColor:        #545f65
+        '') [ "UXTerm" "XTerm" ]}
+      ''}"
+    '';
+   };
+
+   services.timesyncd = {
+     servers = [ "ntp.canterbury.ac.uk" ];
+   };
+
+   #### Machine-specific packages configuration ####
+
+   vuizvui.user.devhell.profiles.packages.enable = true;
+
+   nixpkgs.config.mpv.vaapiSupport = true;
+   nixpkgs.config.mpv.bs2bSupport = true;
+
+   environment.systemPackages = with pkgs; [
+     glxinfo
+     libva
+     libvdpau-va-gl
+     teams
+     vaapiVdpau
+     vdpauinfo
+     xbindkeys
+   ];
+}
diff --git a/machines/devhell/hildr.nix b/machines/devhell/hildr.nix
new file mode 100644
index 00000000..41cfbda9
--- /dev/null
+++ b/machines/devhell/hildr.nix
@@ -0,0 +1,215 @@
+{ config, pkgs, lib, ... }:
+
+{
+  vuizvui.user.devhell.profiles.base.enable = true;
+  vuizvui.system.kernel.bfq.enable = true;
+
+  boot = {
+    loader = {
+      timeout = 2;
+      systemd-boot = {
+        enable = true;
+      };
+
+      efi.canTouchEfiVariables = true;
+    };
+
+    initrd = {
+      availableKernelModules = [ "xhci_hcd" "ahci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
+      kernelModules = [ "fuse" ];
+    };
+
+    kernelModules = [ "kvm-intel" ];
+    extraModulePackages = [ ];
+  };
+
+  hardware = {
+    cpu.intel.updateMicrocode = true;
+    opengl = {
+      enable = true;
+      extraPackages = [ pkgs.libvdpau-va-gl pkgs.vaapiVdpau pkgs.vaapiIntel ];
+    };
+  };
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-uuid/3099f245-51cf-4ca8-b89c-269dbc0ad730";
+    fsType = "btrfs";
+    options = [
+      "space_cache"
+      "compress=zstd"
+      "noatime"
+      "autodefrag"
+    ];
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-uuid/9344-E6FE";
+    fsType = "vfat";
+  };
+
+  swapDevices = [ 
+    { device = "/dev/disk/by-uuid/ff725995-b9a1-453f-9e6d-ba9bd6579db6"; }
+  ];
+
+  # FIXME Check if this is still necessary in the future
+  systemd.services.systemd-networkd-wait-online.enable = false;
+
+  # XXX Ensure that these are added in addition to the DHCP provided DNS servers
+  systemd.network.networks."99-main".dns = [ "1.1.1.1" "1.0.0.1" ];
+
+  networking = {
+    hostName = "hildr";
+    wireless.iwd.enable = true;
+    useNetworkd = true;
+    interfaces = {
+      enp0s31f6.useDHCP = true;
+      wlan0.useDHCP = true;
+    };
+  };
+
+  powerManagement = {
+    powertop.enable = true;
+    cpuFreqGovernor = "powersave";
+  };
+
+  virtualisation.docker.enable = true;
+
+  nix = {
+    maxJobs = lib.mkDefault 4;
+    extraOptions = ''
+      auto-optimise-store = true
+    '';
+  };
+
+  i18n = {
+    defaultLocale = "en_GB.UTF-8";
+  };
+
+  console = {
+    font = "Lat2-Terminus16";
+    keyMap = "uk";
+  };
+
+  #### Machine-specific service configuration ####
+
+  vuizvui.user.devhell.profiles.services.enable = true;
+
+  services = {
+    thermald.enable = true;
+    tftpd.enable = false;
+    gnome3.gnome-keyring.enable = true;
+    printing = {
+      enable = true;
+      drivers = [ pkgs.foo2zjs pkgs.cups-brother-hl1110 ];
+    };
+    offlineimap = {
+      enable = true;
+      install = true;
+      path = [ pkgs.notmuch ];
+    };
+    syncthing = {
+      enable = true;
+      user = "dev";
+      dataDir = "/home/dev/syncthing/";
+    };
+  };
+
+  services.acpid = {
+    enable = true;
+    lidEventCommands = ''
+      LID="/proc/acpi/button/lid/LID/state"
+      state=`cat $LID | ${pkgs.gawk}/bin/awk '{print $2}'`
+      case "$state" in
+        *open*) ;;
+        *close*) systemctl suspend ;;
+        *) logger -t lid-handler "Failed to detect lid state ($state)" ;;
+      esac
+    '';
+  };
+
+  services.xserver = {
+    enable = true;
+    layout = "gb";
+    videoDrivers = [ "modesetting" ];
+
+    libinput = {
+      enable = true;
+      disableWhileTyping = true;
+      middleEmulation = true;
+    };
+#    synaptics = {
+#      enable = true;
+#      twoFingerScroll = true;
+#      palmDetect = true;
+#    };
+
+    # XXX: Factor out and make DRY, because a lot of the stuff here is
+    # duplicated in the other machine configurations.
+    displayManager.sessionCommands = ''
+      ${pkgs.xbindkeys}/bin/xbindkeys &
+      ${pkgs.nitrogen}/bin/nitrogen --restore &
+      ${pkgs.xscreensaver}/bin/xscreensaver -no-splash &
+      ${pkgs.rofi}/bin/rofi &
+      ${pkgs.xorg.xrdb}/bin/xrdb "${pkgs.writeText "xrdb.conf" ''
+        Xft.dpi:                     96
+        Xft.antialias:               true
+        Xft.hinting:                 full
+        Xft.hintstyle:               hintslight
+        Xft.rgba:                    rgb
+        Xft.lcdfilter:               lcddefault
+        Xft.autohint:                1
+        Xcursor.theme:               Vanilla-DMZ-AA
+        Xcursor.size:                22
+        *.charClass:33:48,35:48,37:48,43:48,45-47:48,61:48,63:48,64:48,95:48,126:48,35:48,58:48
+        *background:                 #121212
+        *foreground:                 #babdb6
+        ${lib.concatMapStrings (xterm: ''
+            ${xterm}.termName:       xterm-256color
+            ${xterm}*bellIsUrgent:   true
+            ${xterm}*utf8:           1
+            ${xterm}*locale:             true
+            ${xterm}*utf8Title:          true
+            ${xterm}*utf8Fonts:          1
+            ${xterm}*utf8Latin1:         true
+            ${xterm}*dynamicColors:      true
+            ${xterm}*eightBitInput:      true
+            ${xterm}*faceName:           xft:DejaVu Sans Mono for Powerline:pixelsize=9:antialias=true:hinting=true
+            ${xterm}*faceNameDoublesize: xft:Unifont:pixelsize=12:antialias=true:hinting=true
+            ${xterm}*cursorColor:        #545f65
+        '') [ "UXTerm" "XTerm" ]}
+      ''}"
+    '';
+  };
+
+  #### Machine-specific packages configuration ####
+
+  vuizvui.user.devhell.profiles.packages.enable = true;
+
+  nixpkgs.config.mpv.vaapiSupport = true;
+
+  programs.light.enable = true;
+
+  environment.systemPackages = with pkgs; [
+    aircrackng
+    cdrtools
+    docker
+    dvdplusrwtools
+    horst
+    ipmitool
+    iw
+    kismet
+    libva
+    libvdpau-va-gl
+    minicom
+    pmtools
+    pmutils
+    reaverwps
+    signal-desktop
+    snort
+    teams
+    vaapiVdpau
+    vdpauinfo
+    wavemon
+    xbindkeys
+  ];
+}
diff --git a/machines/devhell/sigrun.nix b/machines/devhell/sigrun.nix
new file mode 100644
index 00000000..6468dc9a
--- /dev/null
+++ b/machines/devhell/sigrun.nix
@@ -0,0 +1,280 @@
+{ config, pkgs, lib, ... }:
+
+{
+  vuizvui.user.devhell.profiles.base.enable = true;
+  vuizvui.system.kernel.bfq.enable = true;
+
+  boot = {
+    loader.grub = {
+      enable = true;
+      version = 2;
+      devices = [
+        "/dev/disk/by-id/ata-ST31500541AS_6XW0NK21"
+        "/dev/disk/by-id/ata-ST31500541AS_6XW0P0CW"
+        "/dev/disk/by-id/ata-Samsung_SSD_850_EVO_250GB_S21PNSAG848626F"
+        "/dev/disk/by-id/ata-Samsung_SSD_850_EVO_250GB_S21PNSAG848674K"
+      ];
+    };
+
+    initrd = {
+      availableKernelModules = [ "ehci_pci" "ahci" "firewire_ohci" "usbhid" "usb_storage" ];
+      kernelModules = [ "fuse" ];
+    };
+
+    kernelParams = [ "pci=noaer" ];
+    kernelModules = [ "kvm-intel" ];
+    extraModulePackages = [ ];
+    blacklistedKernelModules = [ "pcspkr" ];
+  };
+
+  hardware = {
+    cpu.intel.updateMicrocode = true;
+    opengl = {
+      extraPackages = [ pkgs.vaapiVdpau ];
+    };
+  };
+
+  fileSystems."/" = {
+    label = "nixos";
+    fsType = "btrfs";
+    options = [
+      "autodefrag"
+      "space_cache"
+      "compress=lzo"
+      "noatime"
+      "ssd"
+    ];
+  };
+
+  fileSystems."/home" = {
+    label = "home";
+    fsType = "btrfs";
+    options = [
+      "autodefrag"
+      "space_cache"
+      "compress=lzo"
+      "noatime"
+    ];
+  };
+
+  swapDevices = [
+    { device = "/dev/disk/by-uuid/16bd9abd-6af5-4a24-8ea5-58adc51e9641"; }
+    { device = "/dev/disk/by-uuid/279708cb-f9c3-4a37-a064-80ff85a66f88"; }
+    { device = "/dev/disk/by-uuid/0c2409c3-e824-4759-a9ad-9bfcea1e73bb"; }
+    { device = "/dev/disk/by-uuid/3f1835a8-5587-4963-9b6c-66ecb36059de"; }
+  ];
+
+  networking.hostName = "sigrun";
+  networking.wireless.enable = false;
+  networking.useNetworkd = true;
+
+  nix.maxJobs = 8;
+
+  i18n = {
+    defaultLocale = "en_US.UTF-8";
+  };
+
+  console = {
+    font = "Lat2-Terminus16";
+    keyMap = "dvorak";
+  };
+
+  powerManagement.powerUpCommands = ''
+    ${pkgs.hdparm}/sbin/hdparm -B 255 \
+  /dev/disk/by-id/ata-ST31500541AS_6XW0NK21
+    ${pkgs.hdparm}/sbin/hdparm -B 255 \
+  /dev/disk/by-id/ata-ST31500541AS_6XW0P0CW
+  '';
+
+  #### Machine-specific service configuration ####
+
+  vuizvui.user.devhell.profiles.services.enable = true;
+
+  services = {
+    printing = {
+      enable = true;
+      drivers = [ pkgs.hplipWithPlugin ];
+    };
+    thermald.enable = true;
+    timesyncd.enable = true;
+    resolved.enable = true;
+    canto-daemon.enable = true;
+    offlineimap = {
+      enable = true;
+      install = true;
+      path = [ pkgs.notmuch ];
+    };
+    syncthing = {
+      enable = true;
+      user = "dev";
+      dataDir = "/home/dev/syncthing/";
+    };
+  };
+
+  services.xserver = {
+    enable = true;
+    layout = "dvorak";
+    videoDrivers = [ "ati" ];
+
+    serverLayoutSection = ''
+      Screen "Center/Right"
+      Screen "Left" LeftOf "Center/Right"
+    '';
+
+    config = ''
+      Section "ServerLayout"
+        Identifier          "Multihead layout"
+        Screen              "Center/Right"
+        Screen              "Left" LeftOf "Center/Right"
+      EndSection
+
+      Section "Device"
+        Identifier          "Radeon HD 4650 PCIEx8"
+        Driver              "radeon"
+        BusId               "PCI:2:0:0"
+
+        Option              "monitor-DVI-1" "Left monitor"
+      EndSection
+
+      Section "Device"
+        Identifier          "Radeon HD 4650 PCIEx16"
+        Driver              "radeon"
+        BusID               "PCI:1:0:0"
+
+        Option              "monitor-DVI-0" "Center monitor"
+        Option              "monitor-HDMI-0" "Right monitor"
+      EndSection
+
+      Section "Screen"
+        Identifier          "Center/Right"
+        Monitor             "Left monitor"
+        Device              "Radeon HD 4650 PCIEx16"
+      EndSection
+
+      Section "Screen"
+        Identifier          "Left"
+        Device              "Radeon HD 4650 PCIEx8"
+      EndSection
+
+      Section "Monitor"
+        Identifier          "Left monitor"
+      EndSection
+
+      Section "Monitor"
+        Identifier          "Center monitor"
+        Option              "LeftOf" "Right monitor"
+        Option              "Primary" "true"
+      EndSection
+
+      Section "Monitor"
+        Identifier          "Right monitor"
+      EndSection
+    '';
+
+    # XXX: Factor out and make DRY, because a lot of the stuff here is
+    # duplicated in the other machine configurations.
+    displayManager.sessionCommands = ''
+      ${pkgs.xorg.xsetroot}/bin/xsetroot -solid black
+      ${pkgs.xscreensaver}/bin/xscreensaver -no-splash &
+      ${pkgs.rofi}/bin/rofi &
+      ${pkgs.xorg.xrdb}/bin/xrdb "${pkgs.writeText "xrdb.conf" ''
+        Xft.dpi:              96
+        Xft.antialias:        true
+        Xft.hinting:          full
+        Xft.hintstyle:        hintslight
+        Xft.rgba:             rgb
+        Xft.lcdfilter:        lcddefault
+        Xft.autohint:         1
+        XTerm.termName:       xterm-256color
+        XTerm*bellIsUrgent:   true
+        XTerm*utf8:           1
+        XTerm*locale:         true
+        XTerm*utf8Title:      true
+        XTerm*utf8Fonts:      true
+        XTerm*utf8Latin1:     true
+        XTerm*dynamicColors:  true
+        XTerm*eightBitInput:  true
+        Xcursor.theme:        Vanilla-DMZ-AA
+        Xcursor.size:         22
+        *.charClass:33:48,35:48,37:48,43:48,45-47:48,61:48,63:48,64:48,95:48,126:48,35:48,58:48
+        XTerm*faceName:       xft:DejaVu Sans Mono for Powerline:pixelsize=12:antialias=true:hinting=true
+        XTerm*faceNameDoublesize: xft:Unifont:pixelsize=12:antialias=true:hinting=true
+        XTerm*cursorColor:    #545f65
+        XTerm*saveLines:      10000
+        ! Base16 Twilight
+        ! Scheme: David Hart (http://hart-dev.com)
+        #define base00 #1e1e1e
+        #define base01 #323537
+        #define base02 #464b50
+        #define base03 #5f5a60
+        #define base04 #838184
+        #define base05 #a7a7a7
+        #define base06 #c3c3c3
+        #define base07 #ffffff
+        #define base08 #cf6a4c
+        #define base09 #cda869
+        #define base0A #f9ee98
+        #define base0B #8f9d6a
+        #define base0C #afc4db
+        #define base0D #7587a6
+        #define base0E #9b859d
+        #define base0F #9b703f
+        *.foreground:   base05
+        *.background:   base00
+        *.cursorColor:  base05
+        *.color0:       base00
+        *.color1:       base08
+        *.color2:       base0B
+        *.color3:       base0A
+        *.color4:       base0D
+        *.color5:       base0E
+        *.color6:       base0C
+        *.color7:       base05
+        *.color8:       base03
+        *.color9:       base09
+        *.color10:      base01
+        *.color11:      base02
+        *.color12:      base04
+        *.color13:      base06
+        *.color14:      base0F
+        *.color15:      base07
+        ! ------------------------------------------------------------------------------
+        ! ROFI Color theme & Settings
+        ! ------------------------------------------------------------------------------
+        rofi.modi: run
+        rofi.opacity: 85
+        rofi.width: 100
+        rofi.lines: 3
+        rofi.padding: 450
+        rofi.bw: 0
+        rofi.eh: 2
+        rofi.color-enabled: true
+        rofi.color-window: #393939, #393939, #268bd2
+        rofi.color-normal: #393939, #ffffff, #393939, #268bd2, #ffffff
+        rofi.color-active: #393939, #268bd2, #393939, #268bd2, #205171
+        rofi.color-urgent: #393939, #f3843d, #393939, #268bd2, #ffc39c
+      ''}"
+
+      DISPLAY=:0.1 ${pkgs.windowmaker}/bin/wmaker &
+    '';
+  };
+
+  #### Machine-specific packages configuration ####
+
+  vuizvui.user.devhell.profiles.packages.enable = true;
+
+  nixpkgs.config.mpv.bs2bSupport = true;
+
+  environment.systemPackages = with pkgs; [
+    #ipfs
+    #scummvm
+    abook
+    canto-curses
+    cli-visualizer
+    cmus
+    handbrake
+    hplip
+    nzbget
+    slrn
+  ];
+}
diff --git a/machines/misc/mailserver.nix b/machines/misc/mailserver.nix
new file mode 100644
index 00000000..a9548fcb
--- /dev/null
+++ b/machines/misc/mailserver.nix
@@ -0,0 +1,118 @@
+{ config, pkgs, lib, ... }: let
+  vhostMap = {
+    smtpd_sender_login_maps = [
+      "SELECT username AS allowedUser"
+      "FROM mailbox"
+      "WHERE username='%s' AND active = 1"
+      "UNION SELECT goto FROM alias"
+      "WHERE address='%s' AND active = 1"
+    ];
+
+    virtual_alias_maps = [
+      "SELECT goto"
+      "FROM alias"
+      "WHERE address='%s' AND active = '1'"
+    ];
+
+    virtual_mailbox_domains = [
+      "SELECT domain"
+      "FROM domain"
+      "WHERE domain='%s' AND active = '1'"
+    ];
+
+    virtual_mailbox_maps = [
+      "SELECT maildir"
+      "FROM mailbox"
+      "WHERE username='%s' AND active = '1'"
+    ];
+  };
+
+  mkDbMap = query: "proxy:pgsql:${pkgs.writeText "database.cf" ''
+    hosts = localhost
+    user = postfix
+    dbname = postfix
+    query = ${query}
+  ''}";
+
+in {
+  services.spamassassin.enable = true;
+
+  services.postfix.enable = true;
+  services.postfix.hostname = "mailtest.lan";
+
+  # TODO: This is a dummy, replace it once we know about the real root fs.
+  fileSystems."/".label = "root";
+  boot.loader.grub.device = "nodev";
+
+  vuizvui.services.postfix.enable = true;
+  vuizvui.services.postfix.restrictions = {
+    sender = [
+      "reject_authenticated_sender_login_mismatch"
+      "reject_unknown_sender_domain"
+    ];
+    recipient = [
+      "permit_sasl_authenticated"
+      "permit_mynetworks"
+      "reject_unauth_destination"
+      "reject_invalid_hostname"
+      "reject_non_fqdn_hostname"
+      "reject_non_fqdn_sender"
+      "reject_non_fqdn_recipient"
+      "reject_unknown_reverse_client_hostname"
+    ];
+    helo = [
+      "permit_sasl_authenticated"
+      "permit_mynetworks"
+      "reject_invalid_hostname"
+      "reject_unauth_pipelining"
+      "reject_non_fqdn_hostname"
+    ];
+  };
+
+  services.postfix.extraConfig = ''
+    ${lib.concatStrings (lib.mapAttrsToList (cfgvar: query: ''
+      ${cfgvar} = ${mkDbMap (lib.concatStringsSep " " query)}
+    '') vhostMap)}
+
+    # a bit more spam protection
+    disable_vrfy_command = yes
+
+    smtpd_sasl_type=dovecot
+    smtpd_sasl_path=private/auth_dovecot XXXXXXXXXXXXXXX
+    smtpd_sasl_auth_enable = yes
+    smtpd_sasl_authenticated_header = yes
+    broken_sasl_auth_clients = yes
+
+    proxy_read_maps = ${lib.concatStringsSep " " (map (s: "\$${s}") [
+      "local_recipient_maps" "mydestination" "virtual_alias_maps"
+      "virtual_alias_domains" "virtual_mailbox_maps" "virtual_mailbox_domains"
+      "relay_recipient_maps" "relay_domains" "canonical_maps"
+      "sender_canonical_maps" "recipient_canonical_maps" "relocated_maps"
+      "transport_maps" "mynetworks" "smtpd_sender_login_maps"
+    ])}
+
+    local_transport = virtual
+    virtual_transport = dovecot
+
+    virtual_uid_maps = static:5000 XXXXXXXXXXXX
+    virtual_gid_maps = static:5000 XXXXXXXXXXXX
+
+    smtpd_tls_cert_file=/etc/ssl/mail.crt XXXX: KEYS
+    smtpd_tls_key_file=/etc/ssl/mail.key XXXX: KEYS
+    smtpd_use_tls=yes
+  '';
+
+  services.postfix.extraMasterConf = ''
+    mailman unix - n n - - pipe
+      flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py ''${nexthop} ''${user}
+      # ^^^ FIXME: maybe not needed!
+
+    dovecot unix - n n - - pipe
+      flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -d ''${recipient}
+      # ^^^ FIXME: maybe not needed!
+
+    spamassassin unix - n n - - pipe
+      user=${toString config.ids.uids.spamd} argv=${pkgs.spamassassin}/bin/spamc -f -e /var/setuid-wrappers/sendmail -oi -f ''${sender} ''${recipient}
+      # ^^^ FIXME: maybe not needed!
+  '';
+}
diff --git a/machines/openlab/manual-setup.md b/machines/openlab/manual-setup.md
new file mode 100644
index 00000000..6e7f1d20
--- /dev/null
+++ b/machines/openlab/manual-setup.md
@@ -0,0 +1,24 @@
+# Manual setup for labtops
+
+## Poor man’s setup
+
+### Intro
+
+- download newest nixos setup
+- write to USB stick
+- boot live system
+
+### Install vanilla nixos
+
+- use parted to setup msdos table (mktable) & full-disk partition (mkpart 0% 100%)
+- mkfs.ext4 -L /dev/sda1
+- mount /dev/disk/by-label/labtop /mnt
+- nixos-generate-config --root mnt, comment out a few things in /mnt/etc/nixos/configuration.nix
+- nixos-install
+- set temporary root password (123456), will be overwritten by vuizvui
+- reboot
+
+### Install vuizvui
+
+- boot, login as root
+- follow “installing a machine†documentation from vuizvui wiki
diff --git a/machines/profpatsch/base-server.nix b/machines/profpatsch/base-server.nix
new file mode 100644
index 00000000..921e5d8d
--- /dev/null
+++ b/machines/profpatsch/base-server.nix
@@ -0,0 +1,36 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.profpatsch.server;
+
+in
+{
+  imports = [
+    ./base.nix
+  ];
+
+  options.vuizvui.user.profpatsch.server.sshPort = lib.mkOption {
+    description = "ssh port";
+    # TODO: replace with types.intBetween https://github.com/NixOS/nixpkgs/pull/27239
+    type = with lib.types; addCheck int (x: x >= 0 && x <= 65535);
+    default = 6879;
+  };
+
+  config = {
+
+    programs.mosh.enable = true;
+
+    services.openssh = {
+      enable = true;
+      listenAddresses = [ { addr = "0.0.0.0"; port = cfg.sshPort; } ];
+    };
+
+    networking.firewall = {
+      enable = true;
+      allowPing = true;
+      allowedTCPPorts = [ cfg.sshPort ];
+    };
+
+  };
+
+}
diff --git a/machines/profpatsch/base-workstation.nix b/machines/profpatsch/base-workstation.nix
new file mode 100644
index 00000000..42ed0311
--- /dev/null
+++ b/machines/profpatsch/base-workstation.nix
@@ -0,0 +1,155 @@
+# A base configuration for Thinkpads.
+{ pkgs, lib, ... }:
+let
+  myPkgs = import ./pkgs.nix { inherit pkgs lib myLib; };
+  myLib  = import ./lib.nix  { inherit pkgs lib; };
+
+  philip = myLib.philip;
+
+in {
+
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+
+    ###########
+    # Hardware
+
+    boot.loader = {
+      grub.enable = true;
+      grub.version = 2;
+    };
+
+    hardware.cpu.intel.updateMicrocode = true;
+
+    networking = {
+      # better for untrusted networks
+      firewall = {
+        enable = true;
+        # for manual/temporary stuff
+        allowedTCPPortRanges =
+          [{ from = 9990; to = 9999; }];
+      };
+    };
+
+    console = {
+      font = "lat9w-16";
+      keyMap = "neo";
+    };
+
+    # Enables drivers, acpi, power management
+    vuizvui.hardware.thinkpad.enable = true;
+
+    ###################
+    # Graphical System
+
+    services.xserver = {
+
+      enable = true;
+      layout = "de";
+      xkbVariant = "neo";
+      xkbOptions = "altwin:swap_alt_win";
+      serverFlagsSection = ''
+        Option "StandbyTime" "10"
+        Option "SuspendTime" "20"
+        Option "OffTime" "30"
+      '';
+
+      # otherwise xterm is enabled, creating an xterm that spawns the window manager.
+      desktopManager.xterm.enable = false;
+
+      windowManager.xmonad = {
+        enable = true;
+        enableContribAndExtras = true;
+      };
+
+      displayManager = {
+        sessionCommands = with pkgs; ''
+            #TODO add as nixpkg
+            export PATH+=":$HOME/scripts" #add utility scripts
+            export EDITOR=emacsclient
+            export TERMINAL=${lilyterm-git}/bin/lilyterm
+
+            ${xorg.xset}/bin/xset r rate 250 35
+
+            set-background &
+            # TODO xbindkeys user service file
+            ${lib.getBin xbindkeys}/bin/xbindkeys
+            # synchronize clipboards
+            ${lib.getBin autocutsel}/bin/autocutsel -s PRIMARY &
+          '';
+      };
+
+      synaptics = {
+        enable = true;
+        minSpeed = "0.6";
+        maxSpeed = "1.5";
+        accelFactor = "0.015";
+        twoFingerScroll = true;
+        vertEdgeScroll = false;
+      };
+
+    };
+
+    fonts.fontconfig = {
+      enable = true;
+      defaultFonts = {
+        monospace = [ "Source Code Pro" "DejaVu Sans Mono" ]; # TODO does not work
+        sansSerif = [ "Liberation Sans" ];
+      };
+    };
+
+
+    programs.ssh.startAgent = false;
+
+    ###########
+    # Packages
+
+    environment.sessionVariables = { EDITOR = "${myPkgs.vim}/bin/vim"; };
+
+    environment.systemPackages = with pkgs;
+    let
+      # of utmost necessity for me to function
+      basePkgs = [
+        ripgrep           # file content searcher, > ag > ack > grep
+        lr                # list recursively, ls & find replacement
+        dos2unix          # text file conversion
+        manpages          # system manpages (not included by default)
+        mkpasswd          # UNIX password creator
+        ncdu              # disk size checker
+        smartmontools     # check disk state
+        stow              # dotfile management
+        traceroute        # trace ip routes
+        wirelesstools     # iwlist (wifi scan)
+      ];
+      # minimal set of gui applications
+      guiPkgs = [
+        lilyterm-git      # terminal emulator, best one around
+        dmenu             # minimal launcher
+      ];
+    in basePkgs ++ guiPkgs;
+
+    # friendly user shell
+   programs.fish.enable = true;
+
+    ###########
+    # Services
+
+    # services.openssh.enable = true;
+
+    time.timeZone = "Europe/Paris";
+
+    # bounded journal size
+    services.journald.extraConfig = "SystemMaxUse=50M";
+
+    vuizvui.programs.fish.fasd.enable = true;
+
+    ########
+    # Users
+
+    users.users = { inherit philip; };
+
+  };
+}
diff --git a/machines/profpatsch/base.nix b/machines/profpatsch/base.nix
new file mode 100644
index 00000000..e91a7b12
--- /dev/null
+++ b/machines/profpatsch/base.nix
@@ -0,0 +1,61 @@
+# Base config shared by all machines
+{ pkgs, config, lib, ... }:
+
+let
+  # TODO: inject into every config from outside
+  myLib  = import ./lib.nix  { inherit pkgs lib; };
+  myPkgs = import ./pkgs.nix { inherit pkgs lib myLib; };
+
+in
+{
+  config = {
+    # correctness before speed
+    nix.useSandbox = true;
+
+    # /tmp should never be depended on
+    boot.cleanTmpDir = true;
+
+    programs.bash = {
+      interactiveShellInit = ''
+        alias c='vim /root/vuizvui/machines/profpatsch'
+        alias nsp='nix-shell -p'
+        alias nrs='nixos-rebuild switch'
+        alias tad='tmux attach -d'
+        alias gs='git status'
+
+        # search recursively in cwd for file glob (insensitive)
+        findia () { find -iname "*''${*}*"; }
+        # like findia, but first argument is directory
+        findian () { path="$1"; shift; find $path -iname "*''${*}*"; }
+        # like findian, but searches whole filepath
+        findiap () { path="$1"; shift; find $path -ipame "*''${*}*"; }
+      '';
+    };
+
+    environment.systemPackages = with pkgs; [
+      curl              # transfer data to/from a URL
+      file              # file information
+      git               # version control system
+      htop              # top replacement
+      nmap              # stats about clients in the network
+      rsync             # file syncing tool
+      tmux              # detachable terminal multiplexer
+      wget              # the other URL file fetcher
+      myPkgs.vim        # slight improvement over vi
+      lr                # list recursively, ls & find replacement
+      xe                # xargs with a modern interface
+    ];
+
+    i18n = {
+      defaultLocale = "en_US.UTF-8";
+      extraLocaleSettings = {
+        LC_TIME = "de_DE.UTF-8"; #"en_DK.UTF-8";
+      };
+    };
+
+    # Nobody wants mutable state. :)
+    users.mutableUsers = false;
+
+  };
+
+}
diff --git a/machines/profpatsch/haku.nix b/machines/profpatsch/haku.nix
new file mode 100644
index 00000000..c3b84ec4
--- /dev/null
+++ b/machines/profpatsch/haku.nix
@@ -0,0 +1,210 @@
+{ config, pkgs, lib, ... }:
+
+let
+  myLib  = import ./lib.nix  { inherit pkgs lib; };
+  myPkgs = import ./pkgs.nix { inherit pkgs lib myLib; };
+
+  hakuHostName = "haku.profpatsch.de";
+
+  warpspeedPort = 1338;
+  youtube2audiopodcastPort = "1339";
+  youtube2audiopodcastSubdir = "/halp";
+
+  ethernetInterface = "enp0s20";
+  wireguard = {
+    port = 6889;
+    interface = "wg0";
+    internalNetwork =
+      let genIp = cidr: lastByte: "10.42.0.${toString lastByte}/${toString cidr}";
+      in {
+        addr = genIp 32;
+        range = genIp 24 0;
+        server = genIp 24 1;
+      };
+  };
+
+  myKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNMQvmOfon956Z0ZVdp186YhPHtSBrXsBwaCt0JAbkf/U/P+4fG0OROA++fHDiFM4RrRHH6plsGY3W6L26mSsCM2LtlHJINFZtVILkI26MDEIKWEsfBatDW+XNAvkfYEahy16P5CBtTVNKEGsTcPD+VDistHseFNKiVlSLDCvJ0vMwOykHhq+rdJmjJ8tkUWC2bNqTIH26bU0UbhMAtJstWqaTUGnB0WVutKmkZbnylLMICAvnFoZLoMPmbvx8efgLYY2vD1pRd8Uwnq9MFV1EPbkJoinTf1XSo8VUo7WCjL79aYSIvHmXG+5qKB9ed2GWbBLolAoXkZ00E4WsVp9H philip@nyx";
+
+in
+
+{
+  imports = [
+    ./base-server.nix
+  ];
+
+  config = {
+
+    # TODO abstract out
+    nix.maxJobs = 2;
+    vuizvui.modifyNixPath = false;
+    nix.nixPath = [
+      "vuizvui=/root/vuizvui"
+      "nixpkgs=/root/nixpkgs"
+      # TODO: nicer?
+      "nixos-config=${pkgs.writeText "haku-configuration.nix" ''
+        (import <vuizvui/machines>).profpatsch.haku.config
+      ''}"
+    ];
+
+    system.autoUpgrade = {
+      enable = true;
+      channel = "https://headcounter.org/hydra/channel/custom/openlab/vuizvui/channels.machines.profpatsch.haku";
+    };
+
+    vuizvui.user.profpatsch.server.sshPort = 7001;
+
+    boot.loader.grub.device = "/dev/sda";
+    # VPN support
+    boot.extraModulePackages = [ config.boot.kernelPackages.wireguard ];
+
+    fileSystems = {
+      "/" = {
+        device = "/dev/sda3";
+        fsType = "ext4";
+      };
+      "/boot" = {
+        device = "/dev/sda2";
+        fsType = "ext4";
+      };
+    };
+
+    environment.systemPackages = with pkgs; [
+      rtorrent                          # bittorrent client
+      mktorrent                         # torrent file creator
+      pkgs.vuizvui.profpatsch.warpspeed # trivial http file server
+    ];
+
+    users.users = {
+      root.openssh.authorizedKeys.keys = [ myKey ];
+
+      rtorrent = {
+        isNormalUser = true;
+      };
+      vorstand = {
+        isNormalUser = true;
+        openssh.authorizedKeys.keys = [ myKey
+          "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCUgS0VB5XayQobQfOi0tYeqpSSCXzftTKEyII4OYDhuF0/CdXSqOIvdqnWQ8933lPZ5234qCXCniIlRJpJQLBPJdJ7/XnC6W37asuft6yVYxTZnZat8edCuJETMvwZJZNttxHC04k3JPf9RMj25luICWabICH5XP9Mz3GoWSaOz7IOm7jiLQiF3UtiFOG06w76d3UfcIVbqjImwWv8nysphi9IQfL0XgC24zNE6LSeE7IN5xTOxoZxORQGsCEnFNCPevReNcSB0pI9xQ1iao7evaZkpzT4D4iQ/K7Ss8dsfFWN30NPMQS5ReQTUKtmGn1YlgkitiYTEXbMjkYbQaQr daniel@shadow"
+          "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtfWeIH7YZpWUUOZ3oC5FB2/J+P3scxm29gUQdVij/K0TuxW1yN/HtcvrO1mwSshS6sNZ2N6/Kb6+kuGyx1mEnaFt87K5ucxC7TNqiURh4eeZE1xX7B5Ob8TVegrBxoe+vcfaoyxn7sUzgF719H0aYC7PP6p3AIbhq3hRLcvY26u9/gZ39H79A71wCunauvpcnpb+rqyJMN6m2YoeOcoloe7wUDI8Xw5dUetHpNKn9k1vzS16CdwP4pAKI8aBtdNK7ZojVMe9LfBG8HHPr9K+cwcaxQuXkFBJzrfrtBCfQwrgWppsu/W/kGBs1ybku2bOFI5UXJBnsraXQqr1NLIfL phj@phj-X220"
+          "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDj8dla7nOE7RSho2/9LAn+DANYkB1BmMoNryzTQ5mUJWukf5coCc+aNJcXYeu5dSTEicW2qQuD8mt8SDI5Qzv4oSpIYEsd0j4eW/BlC5XYd+4jS7Hfk/a1mJjMG7jdvOUtK3lLtrKaHxVUUjqdxKzzFBZlPov6FgHSJ//h1HxreV/Y0jL94qSvK39FZde5xlV/wQBvpglrMNu7FFWqyeKrOZ7U8D70scFliIuPok/02iQ31P+ncUfV3XrFyJodQq8J3hYEorGVKp3nNM1zaLlg8uqHk18Zt0GFnEAClBrC13yjM0jpMvaMyuXMaWuKeqsBZeUyaSo1j6BNsW/bFjiJ thomas-glamsch@gmx.de"
+          "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCneS9f0u6sITEzKULgIK7LGKskPpyXlQoSLB6aYAS9ZUkZXHh97XwIQqv20/edsAwpKhSSw//n08bvlYjpUSDAg9V/iZzdEV1M7fxek1c0rxtFxbeCds5K67JV+wGEusVyQdtmVzyshBBg+Mk/66E3KgqMyjCyGPk/0qWaj6187DOxerbXI5QkO7lCPIa5jP2YR4yzmFomcGb4PqpPlWfOcjjEjtpenQkhy7iS0ukfkQ9jhIcppuvkZ9A8PL/ccHDNVg0YKNJbsviB5MwKm/6drK87fprP9SP0i7QZsdKhaaW3rQtzCiWup4Avwx91VfeLvef8JJnmDrx+tR7azdGCiiRQvLakT2aIyMSTcDG41PYsesFvqBhRidSgzdZ4I2jjT1iHL4XREQVSjB5voAFdXHrndZj9PT8mTMo1RsdM1YMspHD1ohn2a3YkzKHD6g1n0UM379lyJ2mBEA1w+Nb48s60Mecuy2MlFgN6MbwYXifJkm806nI09ExEfgan8JvWOUQLgCDtp4mGt62vYMmBb5UqyUpvTcPbxoAbpHx+LEAD9Q0OE8S8WGnmkXxnnP2fL1fkcL1wjwvDbW0q9ezl9SrMfxcd+n46kbkYnSJ8HwZSZPX3wn2FAqIAKR+46BSsXW5FTo0xwR9wEjaBC7/6PxcxmAivGJZzYUZ0cjCCOw== lisanne.wolters@gmx.net"
+        ];
+      };
+
+      youtube2audiopodcast = {
+        isSystemUser = true;
+      };
+    };
+
+    systemd.services.warpspeed =
+      let user = config.users.users.rtorrent;
+      in {
+        description = "internally served public files (see nginx)";
+        wantedBy = [ "default.target" ];
+        serviceConfig.WorkingDirectory = "${user.home}/public";
+        # *6: all hosts, v6 preferred
+        script = ''${pkgs.vuizvui.profpatsch.warpspeed}/bin/warpspeed "*6" ${toString warpspeedPort}'';
+        serviceConfig.User = config.users.users.rtorrent.name;
+      };
+
+    systemd.services.youtube2audiopodcast =
+      let user = config.users.users.youtube2audiopodcast;
+      in {
+        description = "serve a youtube playlist as rss";
+        wantedBy = [ "default.target" ];
+        script = "${pkgs.vuizvui.profpatsch.youtube2audiopodcast {
+          url = "https://${hakuHostName}${youtube2audiopodcastSubdir}";
+          internalPort = youtube2audiopodcastPort;
+        }}";
+        serviceConfig.User = config.users.users.youtube2audiopodcast.name;
+      };
+
+
+    security.acme.acceptTerms = true;
+    security.acme.email = "mail@profpatsch.de";
+
+    services.nginx = {
+      enable = true;
+      virtualHosts.${hakuHostName} = {
+        forceSSL = true;
+        enableACME = true;
+        locations."/pub/" = {
+          proxyPass = "http://127.0.0.1:${toString warpspeedPort}/";
+        };
+        locations."${youtube2audiopodcastSubdir}/" = {
+          proxyPass = "http://127.0.0.1:${toString youtube2audiopodcastPort}/";
+        };
+        locations."/".root =
+          let lojbanistanSrc = pkgs.fetchFromGitHub {
+            owner = "lojbanistan";
+            repo = "lojbanistan.de";
+            rev = "ef02aa8f074d0d5209839cd12ba7a67685fdaa05";
+            sha256 = "1hr2si73lam463pcf25napfbk0zb30kgv3ncc0ahv6wndjpsvg7z";
+          };
+          in pkgs.runCommandLocal "lojbanistan-www" {} ''
+            mkdir $out
+            echo "coi do" > $out/index.html
+            ${pkgs.imagemagick}/bin/convert \
+              ${lojbanistanSrc}/design/flag-of-lojbanistan-icon.svg \
+              -define icon:auto-resize=64,48,32,16 \
+              $out/favicon.ico
+          '';
+        serverAliases = [ "lojbanistan.de" ];
+      };
+    };
+
+    networking = {
+      nat = {
+        enable = true;
+        externalInterface = ethernetInterface;
+        internalInterfaces = [ wireguard.interface ];
+      };
+
+      hostName = "haku";
+      firewall = {
+        allowedTCPPorts = [
+          80 443
+          6882
+          1337 2342 4223
+          60100
+        ];
+        allowedUDPPorts = [
+          wireguard.port
+          60100
+        ];
+        # forward wireguard connections to ethernet device (VPN)
+        extraCommands = ''
+          iptables -t nat -A POSTROUTING -s ${wireguard.internalNetwork.range} -o ${ethernetInterface} -j MASQUERADE
+        ''
+        # drop every other kind of forwarding, except from wg0 to epn (and bridge wg)
+        + ''
+          iptables -P FORWARD DROP
+          iptables -A FORWARD -i ${wireguard.interface} -o ${ethernetInterface} -j ACCEPT
+          iptables -A FORWARD -o ${wireguard.interface} -i ${ethernetInterface} -j ACCEPT
+          iptables -A FORWARD -i ${wireguard.interface} -o ${wireguard.interface} -j ACCEPT
+        '';
+      };
+
+      wireguard.interfaces.${wireguard.interface} = {
+        ips = [ wireguard.internalNetwork.server ];
+        listenPort = wireguard.port;
+        privateKeyFile = "/root/keys/wg/vpn.priv";
+
+        peers = [
+          { # shiki (TODO: factor out)
+            publicKey = "x3ko/R8PLzcyjVjqot9qmGBb3NrG/4JvgRkIOQMEsUA=";
+            allowedIPs = [ (wireguard.internalNetwork.addr 2) ];
+          }
+          { # mushu
+            publicKey = "Stx6N4/JurtAuYX+43WPOCLBqheE99O6WRvxW+sd3jw=";
+            allowedIPs = [ (wireguard.internalNetwork.addr 3) ];
+          }
+        ];
+      };
+
+      nameservers = [
+        "62.210.16.6"
+        "62.210.16.7"
+      ];
+    };
+  };
+}
diff --git a/machines/profpatsch/lib.nix b/machines/profpatsch/lib.nix
new file mode 100644
index 00000000..ae730824
--- /dev/null
+++ b/machines/profpatsch/lib.nix
@@ -0,0 +1,17 @@
+{ lib, pkgs }:
+rec {
+  fish = pkgs.fish;
+
+  authKeys = ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJhthfk38lzDvoI7lPqRneI0yBpZEhLDGRBpcXzpPSu+V0YlgrDix5fHhBl+EKfw4aeQNvQNuAky3pDtX+BDK1b7idbz9ZMCExy2a1kBKDVJz/onLSQxiiZMuHlAljVj9iU4uoTOxX3vB85Ok9aZtMP1rByRIWR9e81/km4HdfZTCjFVRLWfvo0s29H7l0fnbG9bb2E6kydlvjnXJnZFXX+KUM16X11lK53ilPdPJdm87VtxeSKZ7GOiBz6q7FHzEd2Zc3CnzgupQiXGSblXrlN22IY3IWfm5S/8RTeQbMLVoH0TncgCeenXH7FU/sXD79ypqQV/WaVVDYMOirsnh/ philip@nyx"];
+
+  philip = rec {
+    name = "philip";
+    extraGroups = [ "wheel" "networkmanager" "docker" "vboxuser" "libvirtd" ];
+    uid = 1000;
+    createHome = true;
+    home = "/home/philip";
+    passwordFile = "${home}/.config/passwd";
+    shell = "${lib.getBin fish}/bin/fish";
+    openssh.authorizedKeys.keys = authKeys;
+  };
+}
diff --git a/machines/profpatsch/mikiya.nix b/machines/profpatsch/mikiya.nix
new file mode 100644
index 00000000..db3b6865
--- /dev/null
+++ b/machines/profpatsch/mikiya.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+let
+  myLib  = import ./lib.nix  { inherit pkgs lib; };
+  myPkgs = import ./pkgs.nix { inherit pkgs lib myLib; };
+
+  mkDevice = category: num: uuid: {
+    name = "mikiya-${category}-crypt-${toString num}";
+    device = "/dev/disk/by-uuid/${uuid}";
+    keyFile = "/root/raid.key";
+  };
+
+  systemDevice = "/dev/disk/by-id/ata-MKNSSDCR60GB-DX_MKN1140A0000025162";
+  systemPartition = {
+    name = "mikiya-root";
+    device = "/dev/disk/by-uuid/56910867-ed83-438a-b67c-c057e662c89e";
+  };
+  rootDevice = "/dev/mapper/mikiya-root";
+
+  raidDevices = lib.imap (mkDevice "raid") [
+    "f0069e04-d058-40b3-8f13-92f11c4c2546"
+  ];
+
+
+
+in {
+  imports = [ ./base-server.nix ];
+
+  config = {
+
+    boot = {
+      loader.grub.device = systemDevice;
+      kernelModules = [ "kvm-intel" ];
+      kernelParams = [ "ip=192.168.0.5" ];
+
+      initrd = {
+        network = {
+          enable = true;
+          ssh.enable = true;
+          ssh.authorizedKeys = myLib.authKeys;
+          # we wait until the root device is unlocked (by ssh)
+          postCommands = ''
+            echo "Waiting for ssh unlock of ${rootDevice} (infinitely)"
+            while [ ! -e ${rootDevice} ]; do sleep 1; done
+          '';
+        };
+          availableKernelModules = [
+            "ahci" "xhci_pci" "usb_storage" "usbhid" "sd_mod"
+          # used for ethernet device(s)
+          "r8169"
+          ];
+
+        # decrypt root device
+        luks.devices = [systemPartition];
+      };
+
+    };
+
+    fileSystems."/" = {
+      device = rootDevice;
+      fsType = "ext4";
+      options = [ "ssd" ];
+    };
+    fileSystems."/boot" = {
+      device = "/dev/disk/by-uuid/9aa38aa7-652f-4762-a0c2-b70332b93f4d";
+      fsType = "ext3";
+    };
+
+    nix.maxJobs = 4;
+
+    vuizvui.user.profpatsch.server.sshPort = 22;
+
+    /*
+    # decrypt RAID with key from root
+    environment.etc.crypttab.text =
+      let luksDevice = dev: "${dev.name} ${dev.device} ${dev.keyFile} luks";
+      in concatMapStringsSep "\n" luksDevice raidDevices;
+
+    powerManagement = {
+      # spin down raid drives after 30 minutes
+      powerUpCommand =
+        let driveStandby = drive: "${pkgs.hdparm}/sbin/hdparm -S 241 ${drive.device}";
+        in concatMapStringsSep "\n" driveStandby raidDevices;
+    */
+
+    users.users = { inherit (myLib) philip; };
+
+  };
+
+}
diff --git a/machines/profpatsch/patches/libnotify.patch b/machines/profpatsch/patches/libnotify.patch
new file mode 100644
index 00000000..88bb545e
--- /dev/null
+++ b/machines/profpatsch/patches/libnotify.patch
@@ -0,0 +1,39 @@
+From 15f0781e728700d8c752a4f0d2e8aaffd8c5ae7c Mon Sep 17 00:00:00 2001
+From: Profpatsch <mail@profpatsch.de>
+Date: Wed, 28 Mar 2018 06:35:27 +0200
+Subject: [PATCH] tools/notify-send.c: return error if message show fails
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+If for example the dbus session can’t be reached, `notify_notification_show`
+will fail and return an error.
+---
+ tools/notify-send.c | 11 +++++++++--
+ 1 file changed, 9 insertions(+), 2 deletions(-)
+
+diff --git a/tools/notify-send.c b/tools/notify-send.c
+index abfffe6..04fad7f 100644
+--- a/tools/notify-send.c
++++ b/tools/notify-send.c
+@@ -274,8 +274,15 @@ main (int argc, char *argv[])
+                 }
+         }
+ 
+-        if (!hint_error)
+-                notify_notification_show (notify, NULL);
++        if (!hint_error) {
++                retval = notify_notification_show (notify, &error);
++                if (!retval) {
++                        fprintf (stderr, "notify-send: error showing notification libnotify says: %s\n",
++                                 error->message);
++                        g_error_free (error);
++                        exit (1);
++                }
++        }
+ 
+         g_object_unref (G_OBJECT (notify));
+ 
+-- 
+2.16.2
+
diff --git a/machines/profpatsch/pkgs.nix b/machines/profpatsch/pkgs.nix
new file mode 100644
index 00000000..98a7988c
--- /dev/null
+++ b/machines/profpatsch/pkgs.nix
@@ -0,0 +1,44 @@
+{ pkgs, lib, myLib }:
+
+let
+
+  mpv = pkgs.mpv-with-scripts.override {
+    scripts = [ pkgs.mpvScripts.convert ];
+  };
+
+  beets = pkgs.beets.override { enableAlternatives = true; };
+
+  vim = pkgs.vim_configurable;
+
+  fast-init = pkgs.haskellPackages.callPackage (import "${(pkgs.fetchFromGitHub {
+    owner = "Profpatsch";
+    repo = "fast-init";
+    # TODO fix version
+    rev = "master";
+    sha256 = "03006xzs250knzcyr6j564kn9jf2a6cp3mxkpqsqmmyp6v28w90z";
+  })}/overrides.nix") {};
+
+  pyrnotify =
+    let src = pkgs.fetchFromGitHub {
+          owner = "arnottcr";
+          repo = "weechat-pyrnotify";
+          rev = "5063ba19b5ba7ba3d4ecb2a76ad9e4b7bf89964b";
+          sha256 = "0r07glz7hkmcnp2vl4dy24i9vfsa9shm7k4q0jb47881z0y2dm2p";
+        };
+        notify-send = "${pkgs.libnotify.overrideAttrs (old: {
+          patches = old.patches or [] ++ [ ./patches/libnotify.patch ];
+        })}/bin/notify-send";
+    in pkgs.runCommandLocal "pyrnotify.py" {} ''
+      substitute "${src}/pyrnotify.py" $out \
+        --replace 'notify-send' '${notify-send}'
+    '';
+
+in
+{ inherit
+    mpv
+    beets
+    vim
+    # fast-init
+    pyrnotify
+    ;
+}
diff --git a/machines/profpatsch/shiki.nix b/machines/profpatsch/shiki.nix
new file mode 100644
index 00000000..3e08f774
--- /dev/null
+++ b/machines/profpatsch/shiki.nix
@@ -0,0 +1,439 @@
+{ config, pkgs, unfreeAndNonDistributablePkgs, lib, ... }:
+let
+
+  myLib  = import ./lib.nix  { inherit pkgs lib; };
+  myPkgs = import ./pkgs.nix { inherit pkgs lib myLib; };
+
+in {
+
+  imports = [
+    ./base-workstation.nix
+  ];
+
+  config = {
+
+    #########
+    # Kernel
+
+    boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ahci" ];
+    boot.loader.grub.device = "/dev/disk/by-id/ata-CT500MX500SSD1_1809E130BEE8";
+
+    # VPN support
+    boot.extraModulePackages = [ config.boot.kernelPackages.wireguard ];
+
+    boot.initrd.luks.devices.cryptroot.device = "/dev/disk/by-uuid/2e1c433f-4a54-4f04-9073-3639b66b975d";
+
+    ###########
+    # Hardware
+
+    fileSystems."/" = {
+      device = "/dev/disk/by-uuid/5339f027-df78-437b-8a4c-39b93abc40b9";
+      fsType = "btrfs";
+      options = [ "ssd" "subvol=/katarafs" ];
+    };
+
+    fileSystems."/boot" = {
+      device = "/dev/disk/by-uuid/53042c4f-bbf2-418b-bf85-5d148ab5dda0";
+      fsType = "ext3";
+    };
+
+    hardware.trackpoint = {
+      speed = 280;
+    };
+
+    hardware.pulseaudio = {
+      enable = true;
+      zeroconf.discovery.enable = true;
+      # for Pillars of Eternity
+      support32Bit = true;
+      package = pkgs.pulseaudio.override {
+        bluetoothSupport = true;
+      };
+      extraModules = [ pkgs.pulseaudio-modules-bt ];
+    };
+    # steam
+    # needed by some games (TODO: general module for games)
+    hardware.opengl.driSupport32Bit = true;
+
+    # TODO: kinda broken?
+    # i18n = {
+    #   inputMethod = {
+    #     enabled = "fcitx";
+    #     Japanese input
+    #     fcitx.engines = with pkgs.fcitx-engines; [ mozc ];
+    #   };
+    # };
+    hardware.bluetooth.enable = true;
+    services.blueman.enable = true;
+
+    ######
+    # Nix
+
+    nix.maxJobs = 2;
+    vuizvui.modifyNixPath = false;
+    nix.nixPath = [
+      "vuizvui=${myLib.philip.home}/vuizvui"
+      "nixpkgs=${myLib.philip.home}/nixpkgs"
+      # TODO: nicer?
+      "nixos-config=${pkgs.writeText "shiki-configuration.nix" ''
+        (import <vuizvui/machines>).profpatsch.shiki.config
+      ''}"
+    ];
+
+    nix.distributedBuilds = true;
+    nix.buildMachines = [
+      # access to the nix-community aarch64 build box
+      {
+        hostName = "aarch64.nixos.community";
+        maxJobs = 64;
+        sshKey = "/root/aarch64-build-box/ssh-key";
+        sshUser = "Profpatsch";
+        system = "aarch64-linux";
+        supportedFeatures = [ "big-parallel" ];
+      }
+      # tweag remote builder
+      {
+        hostName = "build01.tweag.io";
+        maxJobs = 24;
+        sshKey = "/root/.ssh/tweag-nix-builder";
+        sshUser = "nix";
+        system = "x86_64-linux";
+        supportedFeatures = [ "big-parallel" ];
+      }
+    ];
+    nix.extraOptions = ''
+      builders-use-substitutes = true
+      auto-optimise-store = true
+    '';
+
+    ##########
+    # Network
+
+    networking.hostName = "shiki";
+
+    networking.networkmanager.enable = true;
+
+    # TODO: bond eth and wifi again
+    # networking.bonds = {
+    #   wifiAndEthernet = {
+    #     interfaces = [ "wlp3s0" "enp0s25" ];
+    #     driverOptions = {
+    #       # how often to check for link failures, i.e. ethernet down (ms)
+    #       miimon = "500";
+    #       primary = "enp0s25";
+    #       primary_reselect = "always";
+    #       mode = "active-backup";
+    #     };
+    #   };
+    # };
+
+    ###########
+    # Packages
+
+    environment.extraOutputsToInstall = [ "devdoc" ];
+    environment.systemPackages = with pkgs;
+    let
+      systemPkgs =
+      [
+        atool                # archive tools
+        gnupg gnupg1compat   # PGP encryption
+        imagemagick          # image conversion
+        jmtpfs               # MTP fuse
+        mosh                 # ssh with stable connections
+        sshfsFuse            # mount ssh machines
+        # TODO move into atool deps
+        unzip                # extract zip archives
+        networkmanagerapplet # for nm-connection-editor
+      ];
+      xPkgs = [
+        dunst             # notification daemon (interfaces with libnotify)
+        # TODO: replace by xscreensaver or i3lock
+        alock             # lock screen
+        libnotify         # notification library
+        xclip             # clipboard thingy
+        xorg.xkill        # X11 application kill
+      ];
+      guiPkgs = [
+        gnome3.adwaita-icon-theme
+        # TODO: get themes to work. See notes.org.
+        gnome3.gnome_themes_standard
+        pavucontrol
+      ];
+      programmingTools = [
+        cabal2nix                    # convert cabal files to nixexprs
+        # myPkgs.fast-init             # fast-init of haskell projects
+        # gitAndTools.git-annex        # version controlled binary file storage
+        # gitAndTools.git-dit          # decentral issue tracking for git
+        # gitAndTools.git-hub          # lightweight GitHub integration
+
+        # TODO: move to user config
+        direnv
+        httpie                       # nice http CLI
+        jq                           # json filter
+        telnet                       # tcp debugging
+        # TODO: make static binaries
+        pkgs.vuizvui.profpatsch.nix-http-serve # serve nix builds and rebuild on reloads
+        pkgs.vuizvui.profpatsch.nman # open man pages in temporary nix shell
+        pkgs.vuizvui.profpatsch.warpspeed    # trivial http file server
+        # pkgs.vuizvui.profpatsch.nix-gen      # generate nix expressions
+        pkgs.vuizvui.profpatsch.watch-server # restart server on code change
+        pkgs.vuizvui.profpatsch.until        # restart until cmd succeeds
+        execline
+        pkgs.vuizvui.profpatsch.dhall
+        pkgs.vuizvui.profpatsch.dhall-flycheck
+      ];
+      documentation = [
+        # mustache-spec NOT IN 16.09
+      ];
+      userPrograms = [
+        # abcde                # high-level cd-ripper with tag support
+        anki mecab kakasi    # spaced repetition system & japanese analyzer
+        # TODO integrate lame into audacity
+        audacity lame.lib    # audio editor and mp3 codec
+        # myPkgs.beets         # audio file metadata tagger
+        firefox              # browser
+        cups                 # print tools, mainly for lp(1)
+        pkgs.vuizvui.profpatsch.droopy # simple HTML upload server
+        # electrum             # bitcoin client
+        emacs                # pretty neat operating system i guess
+        feh                  # brother of meh, displays images in a meh way, but fast
+        filezilla            # FTP GUI business-ready interface framework
+        gimp                 # graphics
+        inkscape             # vector graphics
+        libreoffice          # a giant ball of C++, that sometimes helps with proprietary shitformats
+        myPkgs.mpv           # you are my sun and my stars, and you play my stuff.
+        pass                 # standard unix password manager
+        picard               # jean-luc, music tagger
+        poppler_utils        # pdfto*
+        ranger               # CLI file browser
+        remind               # calender & reminder program
+        unfreeAndNonDistributablePkgs.steam # the one gaming platform
+        youtube-dl           # download videos
+        zathura              # pdf viewer
+      ];
+      userScripts = with pkgs.vuizvui.profpatsch;
+        let
+          di-notify = pkgs.vuizvui.profpatsch.writeExeclineBin "display-infos-notify" {} [
+            "backtick" "-i" "DI" [ "${display-infos}/bin/display-infos" ]
+            "importas" "DI" "DI"
+            "${pkgs.libnotify}/bin/notify-send" "$DI"
+          ];
+        in [
+        display-infos  # show time & battery
+        di-notify      # same, but pipe to libnotify
+        show-qr-code   # display a QR code
+        backlight      # adjust laptop backlight
+        sfttime        # geek time
+      ];
+      mailPkgs = [
+        elinks               # command line browser
+        msmtp                # SMTP client
+        mu                   # mail indexing w/ emacs mode
+      ];
+      nixPkgs = [
+        nix-diff                  # structurally diff two derivations
+        nix-prefetch-scripts      # prefetch store paths from various destinations
+        pkgs.vuizvui.taalo-build  # build derivation on taalo
+      ];
+      tmpPkgs = [
+        # TODO needs user service
+        redshift   # increases screen warmth at night (so i don’t have to feel cold)
+        # pdfjam is the best CLI pdf modification suite
+        (texlive.combine { inherit (texlive) scheme-small pdfjam; })
+        # move script/nix-cache-binary to here
+        cdb
+        taskwarrior tasksh
+      ];
+    in systemPkgs ++ xPkgs ++ guiPkgs
+    ++ programmingTools ++ documentation
+    ++ userPrograms ++ userScripts
+    ++ mailPkgs ++ nixPkgs ++ tmpPkgs;
+
+    ###########
+    # Services
+
+
+    # Automount
+    services.udisks2.enable = true;
+
+    services.logind.extraConfig = ''
+      # want to be able to listen to music while laptop closed
+      LidSwitchIgnoreInhibited=no
+    '';
+
+    # TMP
+
+    vuizvui.services.guix.enable = true;
+    ###################
+    # Graphical System
+
+    services.xserver = {
+      videoDrivers = [ "intel" ];
+    };
+
+    fonts.fonts = with pkgs; [
+      unfreeAndNonDistributablePkgs.corefonts
+      source-han-sans-japanese
+      source-han-sans-korean
+      source-han-sans-simplified-chinese
+      source-code-pro
+      hasklig
+      dejavu_fonts
+      ubuntu_font_family
+      league-of-moveable-type
+      symbola # emoji
+    ];
+
+    services.printing = {
+      enable = true;
+      drivers = [ pkgs.gutenprint pkgs.gutenprintBin pkgs.hplip ];
+    };
+
+    ###########
+    # Programs
+
+    vuizvui.programs.gnupg = {
+      enable = true;
+      agent = {
+        enable = true;
+        sshSupport = true;
+      };
+    };
+
+    vuizvui.user.profpatsch.programs.scanning = {
+      enable = true;
+      #remoteScanners = ''
+      #  hippie.lab
+      #'';
+    };
+
+    # virtualisation.docker.enable = true;
+
+    #######
+    # Misc
+
+    security.pki.certificateFiles = [ "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ];
+
+    ########
+    # Fixes
+
+    # fix for emacs ssh
+    programs.bash.promptInit = "PS1=\"# \"";
+
+    ################
+    # User services
+    systemd.user = lib.mkMerge [
+
+      (lib.mkIf config.vuizvui.programs.gnupg.enable {
+        services.unlock-password-store = {
+          description = "unlock the user password store";
+          wantedBy = [ "default.target" ];
+          # make sure gpg-agent is running
+          wants = [ "gpg-agent.service" ];
+          after = [ "gpg-agent.service" ];
+          serviceConfig = {
+            # use special unlock key in the password store (needs to exist of course)
+            ExecStart = "${lib.getBin pkgs.pass}/bin/pass misc/unlock";
+            StandardOutput = "null";
+          };
+        };
+        timers.unlock-password-store = {
+          description = "unlock password store on system start";
+          wantedBy = [ "timers.target" ];
+          # run ~five seconds after user logs in
+          timerConfig.OnStartupSec = "5s";
+        };
+       })
+
+      {
+        services.mbsync = {
+          description = "mbsync job";
+          wants = [ "notmuch.service" ];
+          before = [ "notmuch.service"];
+          path = [ pkgs.pass ];
+          serviceConfig = {
+            Restart = "no";
+            ExecStart = "${pkgs.isync}/bin/mbsync -a";
+            };
+        };
+        timers.mbsync = {
+          description = "run mbsync job every 15 minutes";
+          wantedBy = [ "timers.target" ];
+          timerConfig = {
+            OnStartupSec="10s";
+            OnUnitActiveSec ="15m";
+          };
+        };
+        services.mu = {
+          description = "mu job";
+          serviceConfig = {
+            Restart = "no";
+            ExecStart = "${pkgs.notmuch}/bin/notmuch new";
+            };
+        };
+      }
+
+      ({
+        services.dunst = {
+          description = "dunst libnotify daemon";
+          serviceConfig = {
+            Type = "dbus";
+            BusName = "org.freedesktop.Notifications";
+            ExecStart =
+              let config = pkgs.writeText "dunst.conf" (lib.generators.toINI {} {});
+              in "${lib.getBin pkgs.dunst}/bin/dunst --config ${config}";
+            Restart = "on-failure";
+          };
+          partOf = [ "graphical-session.target" ];
+          wantedBy = [ "graphical-session.target" ];
+        };
+      })
+
+      ({
+      #   services.pyrnotify-ssh-connection = {
+      #     description = "ssh connection to make pyrnotify work";
+      #     serviceConfig = {
+      #       # TODO: get out of the gpg-agent service directly
+      #       Environment = ''"SSH_AUTH_SOCK=%t/gnupg/S.gpg-agent.ssh"'';
+      #       ExecStart = pkgs.writeScript "pyrnotify-start-ssh" ''
+      #         #!${pkgs.stdenv.shell}
+      #         set -e
+      #         # first delete the socket file if it exists
+      #         # otherwise the forward doesn’t work
+      #         ${lib.getBin pkgs.openssh}/bin/ssh \
+      #           bigmac \
+      #           "rm /home/bigmac/.weechat/pyrnotify.socket"
+      #         # forwards the remote socket over ssh
+      #         # thE options make it disconnect after 45 sec
+      #         # by sending a keepalive packet every 15 seconds
+      #         # and retrying 3 times
+      #         ${lib.getBin pkgs.openssh}/bin/ssh \
+      #           -o ServerAliveInterval=15 \
+      #           -o ServerAliveCountMax=3 \
+      #           -o ExitOnForwardFailure=yes \
+      #           -R /home/bigmac/.weechat/pyrnotify.socket:localhost:8099 \
+      #           -N \
+      #           bigmac
+      #       '';
+      #     };
+      #     requires = [ "gpg-agent.service" ];
+      #     after = [ "gpg-agent.service" ];
+      #   };
+      #   services.pyrnotify-listen = rec {
+      #     description = "get notified about weechat messages";
+      #     serviceConfig = {
+      #       ExecStart = "${lib.getBin pkgs.python
+      #         }/bin/python ${myPkgs.pyrnotify} 8099";
+      #       Restart = "on-failure";
+      #       RestartSec = "5s";
+      #     };
+      #     bindsTo = [ "pyrnotify-ssh-connection.service" ];
+      #     after = [ "pyrnotify-ssh-connection.service" ];
+      #     wantedBy = [ "default.target" ];
+      #   };
+      })
+
+    ];
+
+  };
+}
diff --git a/machines/sternenseemann/fliewatuet.nix b/machines/sternenseemann/fliewatuet.nix
new file mode 100644
index 00000000..a621270a
--- /dev/null
+++ b/machines/sternenseemann/fliewatuet.nix
@@ -0,0 +1,275 @@
+{ config, lib, pkgs, ... }:
+
+let
+  myPkgs = import ./pkgs.nix { inherit pkgs lib; };
+
+in {
+  nixpkgs.config.allowUnfree = true;
+
+  # hardware
+  boot.blacklistedKernelModules = [ "nouveau" "nvidia" ];
+  boot.initrd.availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" ];
+  boot.kernelModules = [ "kvm-intel" ];
+  boot.initrd.luks.devices = [ { device = "/dev/sda2"; name = "crypted"; } ];
+
+  fileSystems."/" = {
+    device = "/dev/dm-0";
+    fsType = "btrfs";
+  };
+  fileSystems."/boot/" = {
+    device = "/dev/sda1";
+    fsType = "vfat";
+  };
+
+  swapDevices = [ ];
+
+  nix.maxJobs = 8;
+  nix.useSandbox = true;
+  nix.extraOptions = "gc-keep-derivations = false";
+
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.timeout = 5;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  # limit journal size
+  services.journald.extraConfig = "SystemMaxUse=100M";
+
+  # sound
+  # fix sound
+  boot.extraModprobeConfig = ''
+  options snd-hda-intel index=1,0 enable_msi=1
+  '';
+
+  hardware.pulseaudio = {
+    enable = true;
+    support32Bit = true;
+    package = pkgs.pulseaudioFull;
+    zeroconf.discovery.enable = true;
+    daemon.config.flat-volumes = "no";
+  };
+
+  hardware.bluetooth.enable = true;
+
+  hardware.opengl.driSupport32Bit = true;
+  hardware.enableRedistributableFirmware = true;
+
+  hardware.trackpoint = {
+    enable = true;
+    emulateWheel = true;
+    speed = 250;
+    sensitivity = 140;
+  };
+
+  networking.hostName = "fliewatuet";
+  networking.firewall.enable = false;
+
+  networking.supplicant = {
+    wlp4s0 = {
+      configFile.path = "/etc/wpa_supplicant.conf";
+      userControlled.enable = true;
+      userControlled.group = "users";
+      driver = "wext";
+      extraConf = ''
+        ap_scan=1
+      '';
+    };
+  };
+
+
+  i18n = {
+    defaultLocale = "en_US.UTF-8";
+  };
+
+  console = {
+    font = "Lat2-Terminus16";
+    keyMap = "de-latin1";
+  };
+
+  time.timeZone = "Europe/Berlin";
+
+  environment.systemPackages = with pkgs; [
+    ## tools
+    remind
+    pass
+    wget
+    curl
+    stow
+    scrot
+    dmenu
+    mosh
+    gnupg
+    signing-party
+    pinentry
+    gpgme
+    sudo
+    silver-searcher
+    graphicsmagick
+    mkpasswd
+    nmap
+    file
+    zip
+    unzip
+    atool
+    manpages
+    man_db
+    sshuttle
+    youtube-dl
+    psmisc
+    bar-xft
+    unison
+    ddate
+    # aspell
+    aspell
+    aspellDicts.en
+    aspellDicts.de
+
+    ## dev
+    git
+    neovim
+    ghc
+    cabal-install
+    haskellPackages.cabal2nix
+
+    ## applications
+    tmux
+    htop
+    mutt
+    tor
+    torbrowser
+    zathura
+    msmtp
+    isync
+    notmuch
+    irssi
+    myPkgs.texlive
+    firefox
+    chromium
+    surf
+    elinks
+    termite
+    imv
+    gimp
+    rawtherapee
+    pavucontrol
+    cbatticon
+    filezilla
+    screen-message
+    jackline
+    w3m
+
+    ## GUI
+    # wm etc.
+    xdotool
+    xbindkeys
+    alock
+    dunst
+    libnotify
+    xorg.xbacklight
+    hicolor_icon_theme
+    xsel
+
+    ## audio / video
+    myPkgs.mpv
+    spotify
+    audacity
+    lame
+    ffmpeg
+    beets
+
+    ## services
+    acpi
+
+    ## games
+    steam
+  ];
+
+  fonts.fontconfig = {
+    defaultFonts = {
+      monospace = [ "Inconsolata" ];
+      sansSerif = [ "Open Sans" ];
+      serif     = [ "Linux Libertine" ];
+    };
+    ultimate = {
+      enable = true;
+      substitutions = "combi";
+    };
+  };
+
+  fonts.fonts = with pkgs; [
+    corefonts
+    opensans-ttf
+    dejavu_fonts
+    inconsolata
+    tewi-font
+    libertine
+    google-fonts
+    shrikhand # because not in google fonts :(
+    xorg.fontbitstream100dpi
+    xorg.fontbitstreamtype1
+    freefont_ttf
+    unifont
+    unifont_upper
+    poly
+    junicode
+  ];
+
+  # to make Ctrl-Shift-t work in termite
+  environment.etc."vte.sh" = { source = "${pkgs.gnome3.vte}/etc/profile.d/vte.sh"; };
+
+  services.openssh.enable = true;
+
+  services.tor = {
+    enable = true;
+    controlPort = 9051;
+  };
+
+  services.printing = {
+    enable = true;
+    drivers = [ pkgs.gutenprint ];
+  };
+
+  services.tlp.enable = true;
+
+  # Enable the X11 windowing system.
+  services.xserver = {
+    enable = true;
+    layout = "de";
+    xkbVariant = "neo";
+
+    desktopManager.xterm.enable = false;
+    windowManager.herbstluftwm.enable = true;
+
+    displayManager = {
+      sessionCommands =
+        ''
+          ${pkgs.redshift}/bin/redshift -c .redshift &
+          ${pkgs.xorg.xmodmap}/bin/xmodmap -e "pointer = 1 25 3 4 5 6 7 8 9"
+          ${pkgs.xbindkeys}/bin/xbindkeys
+          ${pkgs.cbatticon}/bin/cbatticon &
+        '';
+    };
+
+    synaptics.enable = true;
+    synaptics.tapButtons = false;
+    synaptics.twoFingerScroll = false;
+
+    videoDrivers = [ "intel" ];
+  };
+
+  programs.fish.enable = true;
+
+  users.mutableUsers = false;
+  users.users.lukas = {
+    isNormalUser = true;
+    uid = 1000;
+    home = "/home/lukas";
+    shell = "/run/current-system/sw/bin/fish";
+    group = "users";
+    passwordFile = "/home/lukas/.config/passwd";
+    extraGroups = [ "audio" "wheel" "networkmanager" ];
+  };
+
+  system.stateVersion = "unstable";
+
+  programs.ssh.startAgent = false;
+}
diff --git a/machines/sternenseemann/patches/2bwm-config.patch b/machines/sternenseemann/patches/2bwm-config.patch
new file mode 100644
index 00000000..f541666e
--- /dev/null
+++ b/machines/sternenseemann/patches/2bwm-config.patch
@@ -0,0 +1,131 @@
+diff --git a/config.h b/config.h
+index ce0d1d4..1748dfd 100644
+--- a/config.h
++++ b/config.h
+@@ -35,19 +35,13 @@ static const bool inverted_colors = true;
+ static const uint8_t borders[] = {3,5,5,4};
+ /* Windows that won't have a border.*/
+ #define LOOK_INTO "WM_NAME"
+-static const char *ignore_names[] = {"bar", "xclock"};
++static const char *ignore_names[] = {"lemonbar", "bar", "xclock"};
+ ///--Menus and Programs---///
+-static const char *menucmd[]   = { "my_menu.sh", NULL };
+-static const char *gmrun[]     = { "my_menu2.sh",NULL};
+-static const char *terminal[]  = { "urxvtc", NULL };
++static const char *menucmd[]   = { "dmenu_run", NULL };
++static const char *terminal[]  = { "termite", NULL };
+ static const char *click1[]    = { "xdotool","click", "1", NULL };
+ static const char *click2[]    = { "xdotool","click", "2", NULL };
+ static const char *click3[]    = { "xdotool","click", "3", NULL };
+-static const char *vol_up[]    = { "pamixer", "-u", "-i", "3", "--allow-boost", NULL };
+-static const char *vol_down[]  = { "pamixer", "-u", "-d", "3", "--allow-boost", NULL };
+-static const char *vol_mute[]  = { "amixer", "set", "Master", "mute", "-q", NULL };
+-static const char *bright_up[]  = { "light", "-A", "5", NULL };
+-static const char *bright_down[]  = { "light", "-U", "5", NULL };
+ ///--Custom foo---///
+ static void halfandcentered(const Arg *arg)
+ {
+@@ -93,26 +87,15 @@ static key keys[] = {
+     // Kill a window
+     {  MOD ,              XK_q,          deletewin,         {}},
+     // Resize a window
+-    {  MOD |SHIFT,        XK_k,          resizestep,        {.i=TWOBWM_RESIZE_UP}},
+-    {  MOD |SHIFT,        XK_j,          resizestep,        {.i=TWOBWM_RESIZE_DOWN}},
+-    {  MOD |SHIFT,        XK_l,          resizestep,        {.i=TWOBWM_RESIZE_RIGHT}},
+-    {  MOD |SHIFT,        XK_h,          resizestep,        {.i=TWOBWM_RESIZE_LEFT}},
+-    // Resize a window slower
+-    {  MOD |SHIFT|CONTROL,XK_k,          resizestep,        {.i=TWOBWM_RESIZE_UP_SLOW}},
+-    {  MOD |SHIFT|CONTROL,XK_j,          resizestep,        {.i=TWOBWM_RESIZE_DOWN_SLOW}},
+-    {  MOD |SHIFT|CONTROL,XK_l,          resizestep,        {.i=TWOBWM_RESIZE_RIGHT_SLOW}},
+-    {  MOD |SHIFT|CONTROL,XK_h,          resizestep,        {.i=TWOBWM_RESIZE_LEFT_SLOW}},
++    {  MOD |SHIFT,        XK_l,          resizestep,        {.i=TWOBWM_RESIZE_UP}},
++    {  MOD |SHIFT,        XK_a,          resizestep,        {.i=TWOBWM_RESIZE_DOWN}},
++    {  MOD |SHIFT,        XK_i,          resizestep,        {.i=TWOBWM_RESIZE_RIGHT}},
++    {  MOD |SHIFT,        XK_e,          resizestep,        {.i=TWOBWM_RESIZE_LEFT}},
+     // Move a window
+-    {  MOD ,              XK_k,          movestep,          {.i=TWOBWM_MOVE_UP}},
+-    {  MOD ,              XK_j,          movestep,          {.i=TWOBWM_MOVE_DOWN}},
+-    {  MOD ,              XK_l,          movestep,          {.i=TWOBWM_MOVE_RIGHT}},
+-    {  MOD ,              XK_h,          movestep,          {.i=TWOBWM_MOVE_LEFT}},
+-    // Move a window slower
+-    {  MOD |CONTROL,      XK_k,          movestep,          {.i=TWOBWM_MOVE_UP_SLOW}},
+-    {  MOD |CONTROL,      XK_j,          movestep,          {.i=TWOBWM_MOVE_DOWN_SLOW}},
+-    {  MOD |CONTROL,      XK_l,          movestep,          {.i=TWOBWM_MOVE_RIGHT_SLOW}},
+-    {  MOD |CONTROL,      XK_h,          movestep,          {.i=TWOBWM_MOVE_LEFT_SLOW}},
+-    // Teleport the window to an area of the screen.
++    {  MOD ,              XK_l,          movestep,          {.i=TWOBWM_MOVE_UP}},
++    {  MOD ,              XK_a,          movestep,          {.i=TWOBWM_MOVE_DOWN}},
++    {  MOD ,              XK_i,          movestep,          {.i=TWOBWM_MOVE_RIGHT}},
++    {  MOD ,              XK_e,          movestep,          {.i=TWOBWM_MOVE_LEFT}},
+     // Center:
+     {  MOD ,              XK_g,          teleport,          {.i=TWOBWM_TELEPORT_CENTER}},
+     // Center y:
+@@ -131,9 +114,9 @@ static key keys[] = {
+     {  MOD ,              XK_Home,       resizestep_aspect, {.i=TWOBWM_RESIZE_KEEP_ASPECT_GROW}},
+     {  MOD ,              XK_End,        resizestep_aspect, {.i=TWOBWM_RESIZE_KEEP_ASPECT_SHRINK}},
+     // Full screen window without borders
+-    {  MOD ,              XK_x,         maximize,          {.i=TWOBWM_FULLSCREEN}},
++    {  MOD ,              XK_f,         maximize,          {.i=TWOBWM_FULLSCREEN}},
+     //Full screen window without borders overiding offsets
+-    {  MOD |SHIFT ,       XK_x,          maximize,          {.i=TWOBWM_FULLSCREEN_OVERRIDE_OFFSETS}},
++    {  MOD |SHIFT ,       XK_f,          maximize,          {.i=TWOBWM_FULLSCREEN_OVERRIDE_OFFSETS}},
+     // Maximize vertically
+     {  MOD ,              XK_m,          maxvert_hor,       {.i=TWOBWM_MAXIMIZE_VERTICALLY}},
+     // Maximize horizontally
+@@ -162,45 +145,22 @@ static key keys[] = {
+     {  MOD ,              XK_r,          raiseorlower,      {}},
+     // Next/Previous workspace
+     {  MOD ,              XK_v,          nextworkspace,     {}},
+-    {  MOD ,              XK_c,          prevworkspace,     {}},
++    {  MOD ,              XK_x,          prevworkspace,     {}},
+     // Move to Next/Previous workspace
+     {  MOD |SHIFT ,       XK_v,          sendtonextworkspace,{}},
+-    {  MOD |SHIFT ,       XK_c,          sendtoprevworkspace,{}},
++    {  MOD |SHIFT ,       XK_x,          sendtoprevworkspace,{}},
+     // Iconify the window
+     //{  MOD ,              XK_i,          hide,              {}},
+     // Make the window unkillable
+     {  MOD ,              XK_a,          unkillable,        {}},
+     // Make the window appear always on top
+     {  MOD,               XK_t,          always_on_top,     {}},
+-    // Make the window stay on all workspaces
+-    {  MOD ,              XK_f,          fix,               {}},
+-    // Move the cursor
+-    {  MOD ,              XK_Up,         cursor_move,       {.i=TWOBWM_CURSOR_UP_SLOW}},
+-    {  MOD ,              XK_Down,       cursor_move,       {.i=TWOBWM_CURSOR_DOWN_SLOW}},
+-    {  MOD ,              XK_Right,      cursor_move,       {.i=TWOBWM_CURSOR_RIGHT_SLOW}},
+-    {  MOD ,              XK_Left,       cursor_move,       {.i=TWOBWM_CURSOR_LEFT_SLOW}},
+-    // Move the cursor faster
+-    {  MOD |SHIFT,        XK_Up,         cursor_move,       {.i=TWOBWM_CURSOR_UP}},
+-    {  MOD |SHIFT,        XK_Down,       cursor_move,       {.i=TWOBWM_CURSOR_DOWN}},
+-    {  MOD |SHIFT,        XK_Right,      cursor_move,       {.i=TWOBWM_CURSOR_RIGHT}},
+-    {  MOD |SHIFT,        XK_Left,       cursor_move,       {.i=TWOBWM_CURSOR_LEFT}},
+     // Start programs
+     {  MOD ,              XK_Return,     start,             {.com = terminal}},
+-    {  MOD ,              XK_w,          start,             {.com = menucmd}},
+-    {  MOD |SHIFT,        XK_w,          start,             {.com = gmrun}},
++    {  MOD ,              XK_d,          start,             {.com = menucmd}},
+     // Exit or restart 2bwm
+     {  MOD |CONTROL,      XK_q,          twobwm_exit,       {.i=0}},
+-    {  MOD |CONTROL,      XK_r,          twobwm_restart,    {.i=0}},
+     {  MOD ,              XK_space,      halfandcentered,   {.i=0}},
+-    // Fake clicks using xdotool
+-    {  MOD |CONTROL,      XK_Up,         start,             {.com = click1}},
+-    {  MOD |CONTROL,      XK_Down,       start,             {.com = click2}},
+-	{  MOD |CONTROL,      XK_Right,      start,             {.com = click3}},
+-    {  0x000000,          0x1008ff13, start,             {.com = vol_up}},
+-    {  0x000000,          0x1008ff11,  start,             {.com = vol_down}},
+-    {  0x000000,          0x1008ff15, start,             {.com = vol_mute}},
+-    {  0x000000,          0x1008ff02, start,             {.com = bright_up}},
+-    {  0x000000,          0x1008ff03,  start,             {.com = bright_down}},
+     // Change current workspace
+        DESKTOPCHANGE(     XK_1,                             0)
+        DESKTOPCHANGE(     XK_2,                             1)
+@@ -216,7 +176,6 @@ static key keys[] = {
+ static Button buttons[] = {
+     {  MOD        ,XCB_BUTTON_INDEX_1,     mousemotion,   {.i=TWOBWM_MOVE}},
+     {  MOD        ,XCB_BUTTON_INDEX_3,     mousemotion,   {.i=TWOBWM_RESIZE}},
+-//    {  MOD|CONTROL,XCB_BUTTON_INDEX_3,     start,         {.com = menucmd}},
+     {  MOD|SHIFT,  XCB_BUTTON_INDEX_1,     changeworkspace, {.i=0}},
+     {  MOD|SHIFT,  XCB_BUTTON_INDEX_3,     changeworkspace, {.i=1}},
+     {  MOD|ALT,    XCB_BUTTON_INDEX_1,     changescreen,    {.i=1}},
diff --git a/machines/sternenseemann/pkgs.nix b/machines/sternenseemann/pkgs.nix
new file mode 100644
index 00000000..e580c4da
--- /dev/null
+++ b/machines/sternenseemann/pkgs.nix
@@ -0,0 +1,13 @@
+{ pkgs, lib }:
+
+let
+
+  mpv = pkgs.mpv-with-scripts.override {
+    scripts = [ pkgs.mpvScripts.convert ];
+  };
+
+  texlive = with pkgs.texlive; combine { inherit scheme-medium minted units collection-bibtexextra wrapfig libertine enumitem dashrule ifmtarg xstring; };
+
+  urxvt = pkgs.rxvt_unicode-with-plugins.override { plugins = [ pkgs.urxvt_perls ]; };
+
+in { inherit mpv texlive urxvt; }
diff --git a/machines/sternenseemann/schaf.nix b/machines/sternenseemann/schaf.nix
new file mode 100644
index 00000000..89cf7207
--- /dev/null
+++ b/machines/sternenseemann/schaf.nix
@@ -0,0 +1,105 @@
+{ config, pkgs, lib, ... }:
+{
+  boot.loader.grub.enable = false;
+  boot.loader.generic-extlinux-compatible.enable = true;
+
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+
+  services.nixosManual.enable = false;
+
+  nix.binaryCaches = [ "http://nixos-arm.dezgeg.me/channel" ];
+
+  nix.binaryCachePublicKeys = [
+    "nixos-arm.dezgeg.me-1:xBaUKS3n17BZPKeyxL4JfbTqECsT+ysbDJz29kLFRW0=%"
+  ];
+
+  nix.maxJobs = 3;
+  nix.extraOptions = ''
+    gc-keep-derivations = false
+  '';
+
+  nixpkgs.system = "armv7l-linux";
+  hardware.opengl.enable = false;
+  powerManagement.enable = false;
+
+  networking.hostName = "schaf";
+  networking.dhcpcd.allowInterfaces = [ "eth0" ];
+
+  time.timeZone = "Europe/Berlin";
+
+
+  fileSystems = {
+    "/boot" = {
+      device = "/dev/disk/by-label/NIXOS_BOOT";
+      fsType = "vfat";
+    };
+    "/" = {
+      device = "/dev/disk/by-label/NIXOS_SD";
+      fsType = "ext4";
+    };
+    "/home" = {
+      device = "/dev/disk/by-label/SCHAF_HOME";
+      fsType = "ext4";
+    };
+    "/nix" = {
+      device = "/dev/disk/by-label/NIX_SCHAF";
+      fsType = "ext4";
+    };
+  };
+
+  swapDevices = [ { device = "/swapfile"; size = 1024; } ];
+
+  services.openssh.enable = true;
+  services.openssh.permitRootLogin = "without-password";
+  networking.firewall.enable = false;
+
+  services.journald.extraConfig = "SystemMaxUse=10M";
+
+  environment.systemPackages = with pkgs; [
+    (unison.override { enableX11 = false; })
+    vim
+    sudo
+    git
+    dtach
+    cryptsetup
+  ];
+
+  security.apparmor.enable = true;
+  virtualisation.lxc = {
+    enable = true;
+    usernetConfig = ''
+      lukas veth lxcbr0 10
+    '';
+  };
+
+  services.tor.enable = true;
+  services.tor.extraConfig = ''
+    HiddenServiceDir /var/lib/tor/hs
+    HiddenServicePort 22 127.0.0.1:22
+
+    HiddenServiceDir /var/lib/tor/books
+    HiddenServicePorT 22 127.0.0.1:22
+  '';
+
+  users.users.lukas = {
+    isNormalUser = true;
+    uid = 1000;
+    openssh.authorizedKeys.keys = [
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDdsggyI+fin8c9FySJ1QtiwrExUD6UW5HCNQ5gniz1iUPkdxK9Xb1GcVIbQ2l7h9W7hz5w5bksPa6/ngrXoBRgztok1qzNT1eNA3GGhqVNqO7gQdag+vOReIxyHtcFUc/W7wIERC2zLCwfwCC+jAqsjonjZvnJsX0B0Ds6uGJ69B2U7U57/GcLoewEVjimrPl4aFWmnEMlCmse/vYAQLnTSBMskA2PEYMzs+YIlJtzhw3qv5Xuz3+AgRGarPM4mxSK/oIu4bIX8pwVbiX/GtDiqp8wz+hcQXIt4lnY3dvS5KklctB6VJPmXzrlRz9ujcrHmdOnuE4RSGim92QRMZiZVO7ET9G3wmxV/FiXAy9QBIn8xySr41rDdO5PVSwsKFBNOtLxubfeTlQRJ2eblp2ZJsATo1AdvMx+7PI7ZxrtaEuRyaPLnVn21YFq7slRzNeXx4UNxmRcMk/nOsVoMhFRUui+Vxv55aTZ03rThsV5fF02x5hT4zjCv3DjAQlhbNpBO8K1Dx09lubDa8aChqtamD9NQUgG8bhafeHtwUPoPK9yghEzlYKNh/I0Xv/V51lFkZLcfevMnVuKNEaVchBcx6Oh93kJhMpaASQ0UYvICYDU9hrSue42aR6riEx3XUZeBtJcvRKoBQSw0Crs0nzSPz1NOud2LcVGncp5pX/ICw== git@lukasepple.de"
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCb+uVp/SRQnff7JxUIp+VomFrJBpo+ZIU7hyoaln9tAyVx3RW0B5XZlyZJSDXB4G4mn2fc0qpmY7AlEC2be4fzQSC8US5mKOgaoUz0nItdHg8MxDrBCxc8gR6s7/sbupEr0l48M+7GVQOhZV5yKjEF0XN3XnfDpL67tqjPSCxi9KYXLr8zEJCMaE0dKrAWMBUq3P/Q+pdciV1AOvjkrfiFWw1lM+CefOehEp3hwuCkUKOazKIGskx2MymtkFYdIjTeL/WJkT0mpzlUS3uJ3KCCsCgwDBs/hc7Fad4seDEWCAR7sP6OTXcM3Xd23Ygas9ogxLkinIVQzkfOM3eWoQ8JhjZXG2/tnf1JYHappjiBwm3uTxkCy+qRPwiF8+c6J/qHGKC4EPthaZWejpc9ZbZc6xPZEAtPr4MPdC7AtC12uNsJmWfQQVKUuBKAMrkh5LVjIRAfa3pDy1Vzf1wxohH+CVjCp/lpNr9nzhoY1ahAxS+r22zLdmM70R0R1B8PGRRFIIDj7r+0dRG4Oneg1Y9WvuIscrBaqcH9HGS2zfy+r1EvDoXZBQ4jdfQdMp8OHlqOLLV3F/BkMk8NN6rEqZ+flcK++E98ZodIGE4Ekis3eWuyk496d4Tzc5L/tEITl1d6V1GOBbdVNMWJAvL5T3WZVxlrywOcxLjIop9pgcdhnw== git@lukasepple.de"
+      ];
+    shell = "${pkgs.fish}/bin/fish";
+    group = "users";
+    extraGroups = [ "wheel" ];
+  };
+
+  users.users.books = {
+    uid = 1001;
+    isNormalUser = true;
+    group = "users";
+  };
+
+  programs.fish.enable = true;
+
+  system.stateVersion = "unstable";
+}
diff --git a/machines/sternenseemann/schnurrkadse.nix b/machines/sternenseemann/schnurrkadse.nix
new file mode 100644
index 00000000..fc189956
--- /dev/null
+++ b/machines/sternenseemann/schnurrkadse.nix
@@ -0,0 +1,185 @@
+{ config, lib, pkgs, ... }:
+
+let
+  myPkgs = import ./pkgs.nix { inherit pkgs lib; };
+
+in {
+  nixpkgs.config.allowUnfree = true;
+  nixpkgs.system = "i686-linux";
+
+  boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ata_piix" "usb_storage" "floppy" "usblp" "pcspkr" "btusb" ];
+  boot.kernelModules = [ ];
+  boot.extraModulePackages = [ ];
+
+  boot.initrd.luks.devices =
+    [ { name = "schnurrkadse";
+        device = "/dev/disk/by-uuid/544529b8-81cb-4e8e-9b6b-44f828ea2a7b";
+        preLVM = true; } ];
+
+  fileSystems."/" =
+    { device = "/dev/mapper/schnurrkadse-root";
+      fsType = "btrfs"; };
+
+  fileSystems."/boot" =
+    { device = "/dev/disk/by-uuid/e42bd75d-627d-4469-90cb-282dca7fdd4f";
+      fsType = "ext4"; };
+
+  swapDevices = [ { device = "/dev/mapper/schnurrkadse-swap"; } ];
+
+  nix.maxJobs = 1;
+
+  hardware.pulseaudio.enable = true;
+  hardware.pulseaudio.package = pkgs.pulseaudioFull;
+  hardware.pulseaudio.zeroconf.discovery.enable = true;
+
+  hardware.enableRedistributableFirmware = true;
+
+  hardware.trackpoint = {
+    enable = true;
+    emulateWheel = true;
+    speed = 250;
+    sensitivity = 140;
+  };
+
+  boot.loader.grub.enable = true;
+  boot.loader.grub.version = 2;
+  boot.loader.grub.device = "/dev/sda";
+
+  networking.hostName = "schnurrkadse";
+  networking.supplicant = {
+    wlp4s2 = {
+      configFile.path = "/etc/wpa_supplicant.conf";
+      userControlled.enable = true;
+      userControlled.group = "users";
+      driver = "wext";
+      extraConf = ''
+        ap_scan=1
+      '';
+    };
+  };
+
+  i18n = {
+    defaultLocale = "en_US.UTF-8";
+  };
+
+  console = {
+    font = "Lat2-Terminus16";
+    keyMap = "de";
+  };
+
+  time.timeZone = "Europe/Berlin";
+
+  environment.systemPackages = with pkgs; [
+    unzip
+    zip
+    bzip2
+    wget
+    neovim
+    git
+    stow
+    acpi
+    myPkgs.urxvt
+    xsel
+    sudo
+    mosh
+    dmenu
+    bar-xft
+    alock
+    silver-searcher
+    pavucontrol
+    unison
+
+    myPkgs.texlive
+    pythonPackages.pygments
+    python
+
+    elinks
+    torbrowser
+    chromium
+    myPkgs.mpv
+    htop
+    imv
+    screen-message
+    zathura
+    youtube-dl
+    pass
+    aspell
+    aspellDicts.de
+    aspellDicts.en
+
+    mutt
+    notmuch
+    msmtp
+    isync
+    gnupg
+    gpgme
+    w3m
+  ];
+
+  fonts.fontconfig = {
+    defaultFonts = {
+      monospace = [ "Inconsolata" "Source Code Pro" "DejaVu Sans Mono" ];
+      sansSerif = [ "DejaVu Sans" ];
+      serif = [ "Vollkorn" ];
+    };
+    ultimate.enable = true;
+    ultimate.substitutions = "combi";
+  };
+  fonts.fonts = with pkgs; [
+    corefonts
+    dejavu_fonts
+    inconsolata
+    libertine
+    unifont
+    google-fonts
+  ];
+
+  services.openssh.enable = true;
+
+  services.tor.enable = true;
+  services.tor.controlPort = 9051;
+
+  services.printing = {
+    enable = true;
+    drivers = [ pkgs.gutenprint pkgs.hplip ];
+  };
+
+  services.tlp.enable = true;
+
+  services.xserver = {
+    enable = true;
+    layout = "de";
+    xkbVariant = "neo";
+
+    desktopManager.xterm.enable = false;
+
+    windowManager.herbstluftwm.enable = true;
+
+    displayManager = {
+      sessionCommands =
+        ''
+            ${myPkgs.urxvt}/bin/urxvtd -q -f -o
+        '';
+    };
+
+    synaptics.enable = true;
+    synaptics.tapButtons = false;
+    synaptics.twoFingerScroll = false;
+
+    videoDrivers = [ "intel" ];
+  };
+
+  programs.fish.enable = true;
+
+  users.users.lukas = {
+    isNormalUser = true;
+    uid = 1000;
+    shell = "${pkgs.fish}/bin/fish";
+    group = "users";
+    extraGroups = [ "audio" "wheel" "networkmanager" "uucp" ];
+  };
+
+  programs.ssh.startAgent = false;
+
+  system.stateVersion = "unstable";
+}
diff --git a/modules/README.md b/modules/README.md
new file mode 100644
index 00000000..d9dd5851
--- /dev/null
+++ b/modules/README.md
@@ -0,0 +1,49 @@
+This directory contains various NixOS modules.
+
+If you add a module here, make sure that you define all options using a
+`vuizvui.*` namespace, so that the documentation is generated and you don't
+clash with modules from upstream [nixpkgs](https://github.com/NixOS/nixpkgs).
+
+When writing modules, make sure to categorize them accordingly:
+
+<table>
+  <tr>
+    <th>hardware</th>
+    <td>Hardware-related options</td>
+  </tr>
+  <tr>
+    <th>profiles</th>
+    <td>Options for a specific domain (like for example
+        `desktop`, `router`, `music`, ...)
+    </td>
+  </tr>
+  <tr>
+    <th>programs</th>
+    <td>Program-specific configuration options</td>
+  </tr>
+  <tr>
+    <th>services</th>
+    <td>Modules that implement systemd services</td>
+  </tr>
+  <tr>
+    <th>system</th>
+    <td>Everything system-related (like for example kernel)</td>
+  </tr>
+  <tr>
+    <th>tasks</th>
+    <td>Various one-shot services</td>
+  </tr>
+</table>
+
+If a module is highly specific to your own configuration, use the same
+categories but put them under `user/$category/$module`.
+
+Don't forget to add your module to `module-list.nix`, but make sure you have
+options in place to disable them by default.
+
+## Module option reference
+
+There is also a Hydra job for the currently available options which are
+specific to all of the modules listed in `module-list.nix`:
+
+https://headcounter.org/hydra/job/openlab/vuizvui/manual/latest/download/1/manual.html
diff --git a/modules/core/common.nix b/modules/core/common.nix
new file mode 100644
index 00000000..9c9c7a67
--- /dev/null
+++ b/modules/core/common.nix
@@ -0,0 +1,74 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options.vuizvui = {
+    modifyNixPath = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to modify NIX_PATH for vuizvui, so that &lt;nixpkgs&gt; points
+        to the path within the Nix channel instead of the
+        <literal>nixpkgs</literal> or <literal>nixos</literal> channel from the
+        root user.
+      '';
+    };
+
+    enableGlobalNixpkgsConfig = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enabling this links <literal>nixos-config</literal> to be used by
+        <literal>nixpkgs-config</literal>, which essentially means that
+        attributes defined in <option>nixpkgs.config</option> are also in effect
+        for user environments.
+      '';
+    };
+
+    channelName = mkOption {
+      type = types.str;
+      default = "vuizvui";
+      description = ''
+        The channel name which is used to refer to <literal>vuizvui</literal>.
+      '';
+    };
+  };
+
+  config = {
+    # Expose all packages in ../../pkgs as pkgs.vuizvui in modules.
+    nixpkgs.overlays = singleton (pkgs: const {
+      vuizvui = import ../../pkgs { inherit pkgs; };
+    });
+
+    nix.binaryCaches = [ "https://headcounter.org/hydra/" ];
+    nix.binaryCachePublicKeys = [
+      "headcounter.org:/7YANMvnQnyvcVB6rgFTdb8p5LG1OTXaO+21CaOSBzg="
+    ];
+
+    environment.variables.NIXPKGS_CONFIG = let
+      nixpkgsCfg = toString (pkgs.writeText "nixpkgs-try-config.nix" ''
+        if (builtins.tryEval <nixpkgs-config>).success
+        then import <nixpkgs-config>
+        else {}
+      '');
+    in mkIf config.vuizvui.enableGlobalNixpkgsConfig (mkForce nixpkgsCfg);
+
+    nix.nixPath = let
+      rootChannelsPath = "/nix/var/nix/profiles/per-user/root/channels";
+      channelPath = "${rootChannelsPath}/${config.vuizvui.channelName}";
+      nixosConfig = "/etc/nixos/configuration.nix";
+      nixpkgsConfig = "nixpkgs-config=${pkgs.writeText "nixpkgs-config.nix" ''
+        (import ${pkgs.path}/nixos/lib/eval-config.nix {
+          modules = [ ${nixosConfig} ];
+        }).config.nixpkgs.config
+      ''}";
+      nixPath = [
+        "vuizvui=${channelPath}"
+        "nixpkgs=${channelPath}/nixpkgs"
+        "nixos-config=${nixosConfig}"
+        rootChannelsPath
+      ] ++ optional config.vuizvui.enableGlobalNixpkgsConfig nixpkgsConfig;
+    in mkIf config.vuizvui.modifyNixPath (mkOverride 90 nixPath);
+  };
+}
diff --git a/modules/core/lazy-packages.nix b/modules/core/lazy-packages.nix
new file mode 100644
index 00000000..16f6587e
--- /dev/null
+++ b/modules/core/lazy-packages.nix
@@ -0,0 +1,67 @@
+{ config, pkgs, lib, ... }:
+
+let
+  inherit (lib) escapeShellArg;
+  inherit (config.vuizvui) lazyPackages;
+
+  # Encode the store path in base 64 so that the wrapper
+  # doesn't have a direct dependency on the package.
+  encoder = "${escapeShellArg "${pkgs.coreutils}/bin/base64"} -w0";
+  decoder = "${escapeShellArg "${pkgs.coreutils}/bin/base64"} -d";
+
+  # The command used to fetch the store path from the binary cache.
+  fetchSubstitute = "${escapeShellArg "${pkgs.nix}/bin/nix-store"} -r";
+
+  mkWrapper = package: pkgs.runCommandLocal "${package.name}-lazy" {
+    inherit package;
+  } ''
+    encoded="$(echo "$package" | ${encoder})"
+
+    if [ ! -e "$package/bin" ]; then
+      echo "Store path $package doesn't have a \`bin' directory" \
+           "so we can't create lazy wrappers for it. Please" \
+           "remove \`${escapeShellArg package.name}' from" \
+           "\`vuizvui.lazyPackages'." >&2
+      exit 1
+    fi
+
+    for bin in "$package"/bin/*; do
+      [ -x "$bin" -a ! -d "$bin" ] || continue
+      binpath="''${bin#$package/}"
+
+      mkdir -p "$out/bin"
+      ( echo ${escapeShellArg "#!${pkgs.stdenv.shell}"}
+        echo "encoded='$encoded'"
+        echo "binpath='$binpath'"
+        echo -n ${escapeShellArg ''
+          storepath="$(echo "$encoded" | ${decoder})"
+          program="$storepath/$binpath"
+          if [ ! -e "$storepath" ]; then
+            ${fetchSubstitute} "$storepath" > /dev/null || exit $?
+          fi
+          exec "$program" "$@"
+        ''}
+      ) > "$out/bin/$(basename "$bin")"
+      chmod +x "$out/bin/$(basename "$bin")"
+    done
+  '';
+
+  wrappers = map mkWrapper config.vuizvui.lazyPackages;
+
+in {
+  options.vuizvui.lazyPackages = lib.mkOption {
+    type = lib.types.listOf lib.types.package;
+    default = [];
+    example = lib.literalExample "[ pkgs.gimp pkgs.libreoffice ]";
+    description = ''
+      Packages which are built for this system but instead of being a full
+      runtime dependency, only wrappers of all executables that reside in the
+      <literal>bin</literal> directory are actually runtime dependencies.
+
+      As soon as one of these wrappers is executed, the real package is fetched
+      and the corresponding binary is executed.
+    '';
+  };
+
+  config.environment.systemPackages = map mkWrapper lazyPackages;
+}
diff --git a/modules/core/licensing.nix b/modules/core/licensing.nix
new file mode 100644
index 00000000..1a3ffd5f
--- /dev/null
+++ b/modules/core/licensing.nix
@@ -0,0 +1,19 @@
+{ config, lib, ... }:
+
+let
+  overrideConfig = newConfig: import (import ../../nixpkgs-path.nix) {
+    inherit (config.nixpkgs) localSystem;
+    config = config.nixpkgs.config // newConfig;
+  };
+
+in {
+  _module.args = {
+    unfreePkgs = overrideConfig {
+      whitelistedLicenses = [ lib.licenses.unfreeRedistributable ];
+    };
+
+    unfreeAndNonDistributablePkgs = overrideConfig {
+      allowUnfree = true;
+    };
+  };
+}
diff --git a/modules/core/tests.nix b/modules/core/tests.nix
new file mode 100644
index 00000000..46d96509
--- /dev/null
+++ b/modules/core/tests.nix
@@ -0,0 +1,1137 @@
+{ options, config, pkgs, lib, ... }:
+
+let
+  inherit (lib) any elem;
+
+  whichNet = if config.networking.useNetworkd then "networkd" else "scripted";
+
+  mkTest = attrs: if attrs.check then attrs.paths or [ attrs.path ] else [];
+
+  anyAttrs = pred: cfg: any lib.id (lib.mapAttrsToList (lib.const pred) cfg);
+  hasPackage = p: any (x: x.name == p.name) config.environment.systemPackages;
+
+  mkPrometheusExporterTest = name: {
+    check = config.services.prometheus.exporters.${name}.enable;
+    path = ["nixos" "prometheus-exporters" name];
+  };
+
+  upstreamTests = lib.concatMap mkTest [
+    { check = config.services._3proxy.enable;
+      path  = ["nixos" "_3proxy"];
+    }
+    { check = config.security.acme.certs != {};
+      path  = ["nixos" "acme"];
+    }
+    { check = config.services.atd.enable;
+      path  = ["nixos" "atd"];
+    }
+    { check = config.services.automysqlbackup.enable;
+      path  = ["nixos" "automysqlbackup"];
+    }
+    { check = config.services.avahi.enable;
+      path  = ["nixos" "avahi"];
+    }
+    { check = config.services.babeld.enable;
+      path  = ["nixos" "babeld"];
+    }
+    { check = elem "bcachefs" config.boot.supportedFilesystems;
+      path  = ["nixos" "bcachefs"];
+    }
+    { check = config.services.beanstalkd.enable;
+      path  = ["nixos" "beanstalkd"];
+    }
+    { check = config.services.beesd.filesystems != {};
+      path  = ["nixos" "bees"];
+    }
+    { check = config.services.bind.enable;
+      path  = ["nixos" "bind"];
+    }
+    { check = config.services.transmission.enable
+           || config.services.opentracker.enable;
+      path  = ["nixos" "bittorrent"];
+    }
+    { check = config.services.buildkite-agents != {};
+      path  = ["nixos" "buildkite-agents"];
+    }
+    { check = config.vuizvui.createISO;
+      paths = [
+        ["nixos" "boot" "biosCdrom"]
+        ["nixos" "boot" "biosNetboot"]
+        ["nixos" "boot" "biosUsb"]
+        ["nixos" "boot" "uefiCdrom"]
+        ["nixos" "boot" "uefiNetboot"]
+        ["nixos" "boot" "uefiUsb"]
+      ];
+    }
+    { check = true;
+      path  = ["nixos" "boot-stage1"];
+    }
+    { check = config.services.borgbackup.jobs != {}
+           || config.services.borgbackup.repos != {};
+      path  = ["nixos" "borgbackup"];
+    }
+    { check = config.services.buildbot-master.enable
+           || config.services.buildbot-worker.enable;
+      path  = ["nixos" "buildbot"];
+    }
+    { check = config.services.caddy.enable;
+      path  = ["nixos" "caddy"];
+    }
+    { check = config.services.cadvisor.enable;
+      path  = ["nixos" "cadvisor"];
+    }
+    { check = config.services.cassandra.enable;
+      path  = ["nixos" "cassandra"];
+    }
+    { check = config.services.ceph.enable;
+      paths = [
+        ["nixos" "ceph-single-node"]
+        ["nixos" "ceph-multi-node"]
+      ];
+    }
+    { check = config.services.certmgr.enable;
+      path  = ["nixos" "certmgr"];
+    }
+    { check = config.services.cfssl.enable;
+      path  = ["nixos" "cfssl"];
+    }
+    { check = hasPackage pkgs.chromium;
+      path  = ["nixos" "chromium"];
+    }
+    { check = config.services.cjdns.enable;
+      path  = ["nixos" "cjdns"];
+    }
+    { check = config.services.clickhouse.enable;
+      path  = ["nixos" "clickhouse"];
+    }
+    { check = config.services.cloud-init.enable;
+      path  = ["nixos" "cloud-init"];
+    }
+    { check = config.services.codimd.enable;
+      path  = ["nixos" "codimd"];
+    }
+    { check = config.services.consul.enable;
+      path  = ["nixos" "consul"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.hostBridge != null) config.containers;
+      path  = ["nixos" "containers-bridge"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.ephemeral) config.containers;
+      path  = ["nixos" "containers-ephemeral"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.extraVeths != {}) config.containers;
+      path  = ["nixos" "containers-extra_veth"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.localAddress != []) config.containers;
+      path  = ["nixos" "containers-hosts"];
+    }
+    { check = config.boot.enableContainers;
+      path  = ["nixos" "containers-imperative"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.hostAddress  != null
+                        || i.localAddress != null) config.containers;
+      path  = ["nixos" "containers-ip"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.macvlans != []) config.containers;
+      path  = ["nixos" "containers-macvlans"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.interfaces != []) config.containers;
+      path  = ["nixos" "containers-physical_interfaces"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.forwardPorts != []) config.containers;
+      path  = ["nixos" "containers-portforward"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.privateNetwork) config.containers;
+      path  = ["nixos" "containers-restart_networking"];
+    }
+    { check = config.boot.enableContainers
+           && anyAttrs (i: i.tmpfs != []) config.containers;
+      path  = ["nixos" "containers-tmpfs"];
+    }
+    { check = config.services.corerad.enable;
+      path  = ["nixos" "corerad"];
+    }
+    { check = config.services.couchdb.enable;
+      path  = ["nixos" "couchdb"];
+    }
+    { check = config.services.deluge.enable;
+      path  = ["nixos" "deluge"];
+    }
+    { check = config.security.dhparams.enable
+           && config.security.dhparams.stateful;
+      path  = ["nixos" "dhparams"];
+    }
+    { check = config.services.dnscrypt-proxy2.enable;
+      path  = ["nixos" "dnscrypt-proxy2"];
+    }
+    { check = config.virtualisation.docker.enable;
+      paths = [
+        ["nixos" "docker"]
+        ["nixos" "docker-tools"]
+      ];
+    }
+    { check = config.docker-containers != {};
+      path  = ["nixos" "docker-containers"];
+    }
+    { check = with config.virtualisation.docker; enable
+           && package.name == pkgs.docker-edge.name;
+      path  = ["nixos" "docker-edge"];
+    }
+    { check = config.services.dockerRegistry.enable;
+      path  = ["nixos" "docker-registry"];
+    }
+    { check = config.virtualisation.docker.enable
+           && config.virtualisation.docker.storageDriver == "overlay";
+      path  = ["nixos" "docker-tools-overlay"];
+    }
+    { check = config.services.documize.enable;
+      path  = ["nixos" "documize"];
+    }
+    { check = config.services.dokuwiki.enable;
+      path  = ["nixos" "dokuwiki"];
+    }
+    { check = config.services.dovecot2.enable;
+      path  = ["nixos" "dovecot"];
+    }
+    { check = config.security.pam.enableEcryptfs;
+      path  = ["nixos" "ecryptfs"];
+    }
+    { check = config.services.ejabberd.enable;
+      path  = ["nixos" "ejabberd"];
+    }
+    { check = config.services.logstash.enable
+           || config.services.elasticsearch.enable
+           || config.services.kibana.enable;
+      path  = ["nixos" "elk"];
+    }
+    { check = true;
+      path  = ["nixos" "env"];
+    }
+    { check = config.services.etcd.enable;
+      paths = [
+        ["nixos" "etcd"]
+        ["nixos" "etcd-cluster"]
+      ];
+    }
+    { check = config.hardware.fancontrol.enable;
+      path  = ["nixos" "fancontrol"];
+    }
+    { check = config.services.ferm.enable;
+      path  = ["nixos" "ferm"];
+    }
+    { check = hasPackage pkgs.firefox;
+      path  = ["nixos" "firefox"];
+    }
+    { check = config.networking.firewall.enable;
+      path  = ["nixos" "firewall"];
+    }
+    { check = config.programs.fish.enable;
+      path  = ["nixos" "fish"];
+    }
+    { check = config.services.flannel.enable;
+      path  = ["nixos" "flannel"];
+    }
+    { check = config.services.fluentd.enable;
+      path  = ["nixos" "fluentd"];
+    }
+    { check = config.fonts.enableDefaultFonts;
+      path  = ["nixos" "fontconfig-default-fonts"];
+    }
+    { check = config.services.freeswitch.enable;
+      path  = ["nixos" "freeswitch"];
+    }
+    { check = true;
+      path  = ["nixos" "fsck"];
+    }
+    { check = config.services.gotify.enable;
+      path  = ["nixos" "gotify-server"];
+    }
+    { check = config.services.grocy.enable;
+      path  = ["nixos" "grocy"];
+    }
+    { check = config.services.gitea.enable;
+      path  = ["nixos" "gitea"];
+    }
+    { check = config.services.gitlab.enable;
+      path  = ["nixos" "gitlab"];
+    }
+    { check = config.services.gitolite.enable;
+      path  = ["nixos" "gitolite"];
+    }
+    { check = config.services.gitolite.enable
+           && config.services.fcgiwrap.enable;
+      path  = ["nixos" "gitolite-fcgiwrap"];
+    }
+    { check = config.services.glusterfs.enable;
+      path  = ["nixos" "glusterfs"];
+    }
+    { check = config.services.xserver.desktopManager.gnome3.enable;
+      paths = [
+        ["nixos" "gnome3"]
+        ["nixos" "gnome3-xorg"]
+        ["nixos" "installed-tests"]
+      ];
+    }
+    { check = config.services.gocd-agent.enable;
+      path  = ["nixos" "gocd-agent"];
+    }
+    { check = config.services.gocd-server.enable;
+      path  = ["nixos" "gocd-server"];
+    }
+    { check = config.security.googleOsLogin.enable;
+      path  = ["nixos" "google-oslogin"];
+    }
+    { check = config.services.grafana.enable;
+      path  = ["nixos" "grafana"];
+    }
+    { check = with config.services.graphite; carbon.enableCache
+           || carbon.enableAggregator || carbon.enableRelay
+           || web.enable || api.enable || seyren.enable || pager.enable
+           || beacon.enable;
+      path  = ["nixos" "graphite"];
+    }
+    { check = config.services.graylog.enable;
+      path  = ["nixos" "graylog"];
+    }
+    { check = hasPackage pkgs.gvisor;
+      path  = ["nixos" "gvisor"];
+    }
+    { check = config.services.hadoop.hdfs.namenode.enabled
+           || config.services.hadoop.hdfs.datanode.enabled;
+      path  = ["nixos" "hadoop" "hdfs"];
+    }
+    { check = config.services.hadoop.yarn.resourcemanager.enabled
+           || config.services.hadoop.yarn.nodemanager.enabled;
+      path  = ["nixos" "hadoop" "yarn"];
+    }
+    { check = hasPackage pkgs.handbrake;
+      path  = ["nixos" "handbrake"];
+    }
+    { check = config.services.haproxy.enable;
+      path  = ["nixos" "haproxy"];
+    }
+    { check = config.security.apparmor.enable
+           || config.security.forcePageTableIsolation
+           || config.security.hideProcessInformation
+           || config.security.lockKernelModules
+           || config.security.protectKernelImage;
+      path  = ["nixos" "hardened"];
+    }
+    { check = true;
+      path  = ["nixos" "hibernate"];
+    }
+    { check = config.services.hitch.enable;
+      path  = ["nixos" "hitch"];
+    }
+    { check = config.services.home-assistant.enable;
+      path  = ["nixos" "home-assistant"];
+    }
+    { check = config.services.hound.enable;
+      path  = ["nixos" "hound"];
+    }
+    { check = config.services.hydra.enable;
+      path  = ["nixos" "hydra"];
+    }
+    { check = config.services.xserver.windowManager.i3.enable;
+      path  = ["nixos" "i3wm"];
+    }
+    { check = config.services.icingaweb2.enable;
+      path  = ["nixos" "icingaweb2"];
+    }
+    { check = config.programs.iftop.enable;
+      path  = ["nixos" "iftop"];
+    }
+    { check = config.services.ihatemoney.enable;
+      path  = ["nixos" "ihatemoney"];
+    }
+    { check = config.services.incron.enable;
+      path  = ["nixos" "incron"];
+    }
+    { check = config.services.influxdb.enable;
+      path  = ["nixos" "influxdb"];
+    }
+    { check = config.boot.initrd.network.enable;
+      path  = ["nixos" "initrdNetwork"];
+    }
+    { check = config.boot.initrd.network.ssh.enable;
+      path  = ["nixos" "initrd-network-ssh"];
+    }
+    { check = elem "btrfs" config.boot.supportedFilesystems;
+      paths = [
+        ["nixos" "installer" "btrfsSimple"]
+        ["nixos" "installer" "btrfsSubvols"]
+        ["nixos" "installer" "btrfsSubvolDefault"]
+      ];
+    }
+    { check = anyAttrs (f: f.encrypted.enable) config.fileSystems
+           || lib.any (s: s.encrypted.enable) config.swapDevices;
+      path  = ["nixos" "installer" "encryptedFSWithKeyfile"];
+    }
+    { check = config.boot.loader.grub.version == 1;
+      path  = ["nixos" "installer" "grub1"];
+    }
+    { check = config.boot.initrd.luks.devices != [];
+      paths = [
+        ["nixos" "installer" "luksroot"]
+        ["nixos" "installer" "luksroot-format1"]
+        ["nixos" "installer" "luksroot-format2"]
+      ];
+    }
+    { check = true;
+      path  = ["nixos" "installer" "lvm"];
+    }
+    { check = config.fileSystems ? "/boot";
+      path  = ["nixos" "installer" "separateBoot"];
+    }
+    { check = config.fileSystems ? "/boot"
+           && config.fileSystems."/boot".fsType == "vfat";
+      path  = ["nixos" "installer" "separateBootFat"];
+    }
+    { check = elem "ext3" config.boot.supportedFilesystems;
+      path  = ["nixos" "installer" "simple"];
+    }
+    { check = elem "ext3" config.boot.supportedFilesystems
+           && config.nesting.clone != [];
+      path  = ["nixos" "installer" "simpleClone"];
+    }
+    { check = config.boot.loader.grub.device == "nodev"
+           && config.boot.loader.grub.efiSupport;
+      path  = ["nixos" "installer" "simpleUefiGrub"];
+    }
+    { check = config.boot.loader.grub.device == "nodev"
+           && config.boot.loader.grub.efiSupport
+           && config.nesting.clone != [];
+      path  = ["nixos" "installer" "simpleUefiGrubClone"];
+    }
+    { check = config.boot.loader.systemd-boot.enable;
+      path  = ["nixos" "installer" "simpleUefiSystemdBoot"];
+    }
+    { check = config.boot.loader.grub.fsIdentifier == "label";
+      path  = ["nixos" "installer" "simpleLabels"];
+    }
+    { check = config.boot.loader.grub.fsIdentifier == "provided";
+      path  = ["nixos" "installer" "simpleProvided"];
+    }
+    { check = config.boot.initrd.mdadmConf != "";
+      path  = ["nixos" "installer" "swraid"];
+    }
+    { check = elem "zfs" config.boot.supportedFilesystems;
+      path  = ["nixos" "installer" "zfsroot"];
+    }
+    { check = config.networking.enableIPv6;
+      path  = ["nixos" "ipv6"];
+    }
+    { check = config.services.jackett.enable;
+      path  = ["nixos" "jackett"];
+    }
+    { check = config.services.jellyfin.enable;
+      path  = ["nixos" "jellyfin"];
+    }
+    { check = config.services.jenkins.enable;
+      path  = ["nixos" "jenkins"];
+    }
+    { check = config.services.apache-kafka.enable;
+      path  = ["nixos" "kafka"];
+    }
+    { check = config.services.keepalived.enable;
+      path  = ["nixos" "keepalived"];
+    }
+    { check = let
+        isHeimdal = lib.hasPrefix "heimdal" config.krb5.kerberos.name;
+        isServer = config.services.kerberos_server.enable;
+      in isHeimdal && (isServer || config.krb5.enable);
+      path  = ["nixos" "kerberos" "heimdal"];
+    }
+    { check = let
+        isHeimdal = lib.hasPrefix "heimdal" config.krb5.kerberos.name;
+        isServer = config.services.kerberos_server.enable;
+      in !isHeimdal && (isServer || config.krb5.enable);
+      path  = ["nixos" "kerberos" "mit"];
+    }
+    { check = config.boot.kernelPackages.kernel.version
+           == pkgs.linuxPackages_latest.kernel.version;
+      path  = ["nixos" "kernel-latest"];
+    }
+    { check = config.boot.kernelPackages.kernel.version
+           == pkgs.linuxPackages.kernel.version;
+      path  = ["nixos" "kernel-lts"];
+    }
+    { check = config.boot.kernelPackages.kernel.version
+           == pkgs.linuxPackages_testing.kernel.version;
+      path  = ["nixos" "kernel-testing"];
+    }
+    { check = config.console.keyMap              == "azerty/fr"
+           || config.services.xserver.layout     == "fr";
+      path  = ["nixos" "keymap" "azerty"];
+    }
+    { check = config.console.keyMap              == "colemak/colemak"
+           || config.services.xserver.xkbVariant == "colemak";
+      path  = ["nixos" "keymap" "colemak"];
+    }
+    { check = config.console.keyMap              == "dvorak"
+           || config.services.xserver.layout     == "dvorak";
+      path  = ["nixos" "keymap" "dvorak"];
+    }
+    { check = config.console.keyMap              == "dvp"
+           || config.services.xserver.xkbVariant == "dvp";
+      path  = ["nixos" "keymap" "dvp"];
+    }
+    { check = config.console.keyMap              == "neo"
+           || config.services.xserver.xkbVariant == "neo";
+      path  = ["nixos" "keymap" "neo"];
+    }
+    { check = config.console.keyMap              == "de"
+           || config.services.xserver.layout     == "de";
+      path  = ["nixos" "keymap" "qwertz"];
+    }
+    { check = config.services.knot.enable;
+      path  = ["nixos" "knot"];
+    }
+    { check = with config.services.kubernetes; apiserver.enable
+           || scheduler.enable || controllerManager.enable || kubelet.enable
+           || proxy.enable;
+      paths = [
+        ["nixos" "kubernetes" "dns" "singlenode"]
+        ["nixos" "kubernetes" "dns" "multinode"]
+        ["nixos" "kubernetes" "rbac" "singlenode"]
+        ["nixos" "kubernetes" "rbac" "multinode"]
+      ];
+    }
+    { check = config.boot.kernelPackages.kernel.version
+           == pkgs.linuxPackages_latest.kernel.version;
+      path  = ["nixos" "latestKernel" "login"];
+    }
+    { check = config.services.openldap.enable
+           || config.users.ldap.enable;
+      path  = ["nixos" "ldap"];
+    }
+    { check = config.services.leaps.enable;
+      path  = ["nixos" "leaps"];
+    }
+    { check = config.services.lidarr.enable;
+      path  = ["nixos" "lidarr"];
+    }
+    { check = config.services.xserver.displayManager.lightdm.enable;
+      path  = ["nixos" "lightdm"];
+    }
+    { check = config.services.limesurvey.enable;
+      path  = ["nixos" "limesurvey"];
+    }
+    { check = true;
+      path  = ["nixos" "login"];
+    }
+    { check = config.services.loki.enable;
+      path  = ["nixos" "loki"];
+    }
+    { check = hasPackage pkgs.lorri;
+      path  = ["nixos" "lorri"];
+    }
+    { check = config.services.magnetico.enable;
+      path  = ["nixos" "magnetico"];
+    }
+    { check = config.services.mailcatcher.enable;
+      path  = ["nixos" "mailcatcher"];
+    }
+    { check = config.services.mathics.enable;
+      path  = ["nixos" "mathics"];
+    }
+    { check = config.services.matomo.enable;
+      path  = ["nixos" "matomo"];
+    }
+    { check = config.services.matrix-synapse.enable;
+      path  = ["nixos" "matrix-synapse"];
+    }
+    { check = config.services.mediawiki.enable;
+      path  = ["nixos" "mediawiki"];
+    }
+    { check = config.services.memcached.enable;
+      path  = ["nixos" "memcached"];
+    }
+    { check = config.services.mesos.master.enable
+           || config.services.mesos.slave.enable;
+      path  = ["nixos" "mesos"];
+    }
+    { check = config.services.metabase.enable;
+      path  = ["nixos" "metabase"];
+    }
+    { check = config.services.miniflux.enable;
+      path  = ["nixos" "miniflux"];
+    }
+    { check = config.services.minio.enable;
+      path  = ["nixos" "minio"];
+    }
+    { check = config.services.minidlna.enable;
+      path  = ["nixos" "minidlna"];
+    }
+    { check = true;
+      path  = ["nixos" "misc"];
+    }
+    { check = config.services.moinmoin.enable;
+      path  = ["nixos" "moinmoin"];
+    }
+    { check = config.services.mongodb.enable;
+      path  = ["nixos" "mongodb"];
+    }
+    { check = config.services.moodle.enable;
+      path  = ["nixos" "moodle"];
+    }
+    { check = config.services.morty.enable;
+      path  = ["nixos" "morty"];
+    }
+    { check = config.services.mosquitto.enable;
+      path  = ["nixos" "mosquitto"];
+    }
+    { check = config.services.mpd.enable;
+      path  = ["nixos" "mpd"];
+    }
+    { check = config.services.murmur.enable;
+      path  = ["nixos" "mumble"];
+    }
+    { check = config.services.munin-node.enable
+           || config.services.munin-cron.enable;
+      path  = ["nixos" "munin"];
+    }
+    { check = true;
+      path  = ["nixos" "mutableUsers"];
+    }
+    { check = config.services.mxisd.enable;
+      path  = ["nixos" "mxisd"];
+    }
+    { check = config.services.mysql.enable;
+      path  = ["nixos" "mysql"];
+    }
+    { check = config.services.mysqlBackup.enable;
+      path  = ["nixos" "mysqlBackup"];
+    }
+    { check = config.services.mysql.enable
+           && config.services.mysql.replication.role != "none";
+      path  = ["nixos" "mysqlReplication"];
+    }
+    { check = config.services.nagios.enable;
+      path  = ["nixos" "nagios"];
+    }
+    { check = config.networking.nat.enable
+           && config.networking.firewall.enable;
+      path  = ["nixos" "nat" "firewall"];
+    }
+    { check = with config.networking; let
+        isIptables = nat.enable || firewall.enable;
+        hasConntrack = firewall.connectionTrackingModules != []
+                    || firewall.autoLoadConntrackHelpers;
+      in isIptables && hasConntrack;
+      path  = ["nixos" "nat" "firewall-conntrack"];
+    }
+    { check = config.networking.nat.enable
+           && !config.networking.firewall.enable;
+      path  = ["nixos" "nat" "standalone"];
+    }
+    { check = config.services.ndppd.enable;
+      path  = ["nixos" "ndppd"];
+    }
+    { check = config.services.neo4j.enable;
+      path  = ["nixos" "neo4j"];
+    }
+    { check = config.nesting.clone != []
+           || config.nesting.children != [];
+      path  = ["nixos" "nesting"];
+    }
+    { check = config.services.netdata.enable;
+      path  = ["nixos" "netdata"];
+    }
+    { check = config.networking.bonds != {};
+      path  = ["nixos" "networking" whichNet "bond"];
+    }
+    { check = config.networking.bridges != {};
+      path  = ["nixos" "networking" whichNet "bridge"];
+    }
+    { check = anyAttrs (i: i.useDHCP == true) config.networking.interfaces;
+      path  = ["nixos" "networking" whichNet "dhcpOneIf"];
+    }
+    { check = config.networking.useDHCP;
+      path  = ["nixos" "networking" whichNet "dhcpSimple"];
+    }
+    { check = true;
+      path  = ["nixos" "networking" whichNet "loopback"];
+    }
+    { check = config.networking.macvlans != {};
+      path  = ["nixos" "networking" whichNet "macvlan"];
+    }
+    { check = let
+        hasPrivacy = iface: iface.tempAddress == "default"
+                         || iface.tempAddress == "enabled";
+      in anyAttrs hasPrivacy config.networking.interfaces;
+      path  = ["nixos" "networking" whichNet "privacy"];
+    }
+    { check = anyAttrs (i: i.ipv4.routes != [] || i.ipv6.routes != [])
+              config.networking.interfaces;
+      path  = ["nixos" "networking" whichNet "routes"];
+    }
+    { check = config.networking.sits != {};
+      path  = ["nixos" "networking" whichNet "sit"];
+    }
+    { check = anyAttrs (i: i.ipv4.addresses != [])
+              config.networking.interfaces;
+      path  = ["nixos" "networking" whichNet "static"];
+    }
+    { check = anyAttrs (i: i.virtual) config.networking.interfaces;
+      path  = ["nixos" "networking" whichNet "virtual"];
+    }
+    { check = config.networking.vlans != {};
+      path  = ["nixos" "networking" whichNet "vlan"];
+    }
+    { check = with config.networking.proxy; any (val: val != null)
+            [ default allProxy ftpProxy httpProxy httpsProxy noProxy
+              rsyncProxy
+            ];
+      path  = ["nixos" "networkingProxy"];
+    }
+    { check = config.services.nextcloud.enable;
+      path  = ["nixos" "nextcloud" "basic"];
+    }
+    { check = config.services.nextcloud.enable
+           && config.services.nextcloud.config.dbtype == "mysql";
+      path  = ["nixos" "nextcloud" "with-mysql-and-memcached"];
+    }
+    { check = config.services.nextcloud.enable
+           && config.services.nextcloud.config.dbtype == "pgsql";
+      path  = ["nixos" "nextcloud" "with-postgresql-and-redis"];
+    }
+    { check = config.services.nexus.enable;
+      path  = ["nixos" "nexus"];
+    }
+    { check = elem "nfs" config.boot.supportedFilesystems;
+      paths = [
+        ["nixos" "nfs3"]
+        ["nixos" "nfs4"]
+      ];
+    }
+    { check = config.services.nghttpx.enable;
+      path  = ["nixos" "nghttpx"];
+    }
+    { check = config.services.nginx.enable;
+      paths = [
+        ["nixos" "nginx"]
+        ["nixos" "nginx-etag"]
+      ];
+    }
+    { check = config.services.nginx.sso.enable;
+      path  = ["nixos" "nginx-sso"];
+    }
+    { check = config.nix.sshServe.enable;
+      path  = ["nixos" "nix-ssh-serve"];
+    }
+    { check = true;
+      path  = ["nixos" "nixos-generate-config"];
+    }
+    { check = config.services.novacomd.enable;
+      path  = ["nixos" "novacomd"];
+    }
+    { check = config.services.nsd.enable;
+      path  = ["nixos" "nsd"];
+    }
+    { check = config.services.nzbget.enable;
+      path  = ["nixos" "nzbget"];
+    }
+    { check = config.services.openarena.enable;
+      path  = ["nixos" "openarena"];
+    }
+    { check = config.services.openldap.enable;
+      path  = ["nixos" "openldap"];
+    }
+    { check = config.services.opensmtpd.enable;
+      path  = ["nixos" "opensmtpd"];
+    }
+    { check = config.services.openssh.enable;
+      path  = ["nixos" "openssh"];
+    }
+    { check = config.services.orangefs.client.enable
+           || config.services.orangefs.server.enable;
+      path  = ["nixos" "orangefs"];
+    }
+    { check = config.boot.loader.grub.enable
+           && config.boot.loader.grub.useOSProber;
+      path  = ["nixos" "os-prober"];
+    }
+    { check = config.services.osrm.enable;
+      path  = ["nixos" "osrm-backend"];
+    }
+    { check = true;
+      path  = ["nixos" "overlayfs"];
+    }
+    { check = config.services.packagekit.enable;
+      path  = ["nixos" "packagekit"];
+    }
+    { check = config.security.pam.oath.enable;
+      path  = ["nixos" "pam-oath-login"];
+    }
+    { check = config.security.pam.u2f.enable;
+      path  = ["nixos" "pam-u2f"];
+    }
+    { check = config.services.xserver.desktopManager.pantheon.enable;
+      path  = ["nixos" "pantheon"];
+    }
+    { check = config.services.paperless.enable;
+      path  = ["nixos" "paperless"];
+    }
+    { check = config.services.peerflix.enable;
+      path  = ["nixos" "peerflix"];
+    }
+    { check = with config.services.postgresql; enable
+           && lib.any (lib.hasPrefix "pgjwt") extraPlugins;
+      path  = ["nixos" "pgjwt"];
+    }
+    { check = config.services.pgmanage.enable;
+      path  = ["nixos" "pgmanage"];
+    }
+    { check = config.services.httpd.enable
+           && config.services.httpd.enablePHP;
+      path  = ["nixos" "php-pcre"];
+    }
+    { check = config.services.xserver.desktopManager.plasma5.enable;
+      path  = ["nixos" "plasma5"];
+    }
+    { check = config.programs.plotinus.enable;
+      path  = ["nixos" "plotinus"];
+    }
+    { check = with config.services.postgresql; enable
+           && lib.any (lib.hasPrefix "postgis") extraPlugins;
+      path  = ["nixos" "postgis"];
+    }
+    { check = config.services.postgresql.enable;
+      path  = let
+        filterPg = name: drv: lib.hasPrefix "postgresql" name
+                           && drv == config.services.postgresql.package;
+        pgPackage = lib.head (lib.attrNames (lib.filterAttrs filterPg pkgs));
+      in ["nixos" "postgresql" pgPackage];
+    }
+    { check = config.services.postgresqlWalReceiver.receivers != {};
+      path  = ["nixos" "postgresql-wal-receiver"];
+    }
+    { check = config.services.powerdns.enable;
+      path  = ["nixos" "powerdns"];
+    }
+    { check = config.services.pppd.enable;
+      path  = ["nixos" "pppd"];
+    }
+    { check = config.networking.usePredictableInterfaceNames
+           && !config.networking.useNetworkd;
+      path  = ["nixos" "predictable-interface-names" "predictable"];
+    }
+    { check = config.networking.usePredictableInterfaceNames
+           && config.networking.useNetworkd;
+      path  = ["nixos" "predictable-interface-names" "predictableNetworkd"];
+    }
+    { check = !config.networking.usePredictableInterfaceNames
+           && !config.networking.useNetworkd;
+      path  = ["nixos" "predictable-interface-names" "unpredictable"];
+    }
+    { check = !config.networking.usePredictableInterfaceNames
+           && config.networking.useNetworkd;
+      path  = ["nixos" "predictable-interface-names" "unpredictableNetworkd"];
+    }
+    { check = config.services.printing.enable;
+      path  = ["nixos" "printing"];
+    }
+    { check = config.services.prometheus.enable;
+      path  = ["nixos" "prometheus"];
+    }
+    # TODO: Generate automatically!
+    (mkPrometheusExporterTest "bind")
+    (mkPrometheusExporterTest "blackbox")
+    (mkPrometheusExporterTest "collectd")
+    (mkPrometheusExporterTest "dnsmasq")
+    (mkPrometheusExporterTest "dovecot")
+    (mkPrometheusExporterTest "fritzbox")
+    (mkPrometheusExporterTest "json")
+    (mkPrometheusExporterTest "mail")
+    (mkPrometheusExporterTest "nextcloud")
+    (mkPrometheusExporterTest "nginx")
+    (mkPrometheusExporterTest "node")
+    (mkPrometheusExporterTest "postfix")
+    (mkPrometheusExporterTest "postgres")
+    (mkPrometheusExporterTest "rspamd")
+    (mkPrometheusExporterTest "snmp")
+    (mkPrometheusExporterTest "surfboard")
+    (mkPrometheusExporterTest "tor")
+    (mkPrometheusExporterTest "varnish")
+    (mkPrometheusExporterTest "wireguard")
+    { check = config.services.prosody.enable;
+      path  = ["nixos" "prosody"];
+    }
+    { check = with config.services.prosody; enable
+           && builtins.match ".*MySQL.*" extraConfig != null;
+      path  = ["nixos" "prosodyMysql"];
+    }
+    { check = config.services.httpd.enable
+           && elem "proxy_balancer" config.services.httpd.extraModules;
+      path  = ["nixos" "proxy"];
+    }
+    { check = config.services.quagga.ospf.enable;
+      path  = ["nixos" "quagga"];
+    }
+    { check = config.services.rabbitmq.enable;
+      path  = ["nixos" "rabbitmq"];
+    }
+    { check = config.services.radarr.enable;
+      path  = ["nixos" "radarr"];
+    }
+    { check = config.services.radicale.enable;
+      path  = ["nixos" "radicale"];
+    }
+    { check = config.services.redis.enable;
+      path  = ["nixos" "redis"];
+    }
+    { check = config.services.redmine.enable;
+      path  = ["nixos" "redmine"];
+    }
+    { check = config.services.restic.backups != {};
+      path  = ["nixos" "restic"];
+    }
+    { check = config.services.roundcube.enable;
+      path  = ["nixos" "roundcube"];
+    }
+    { check = config.services.rspamd.enable;
+      path  = ["nixos" "rspamd"];
+    }
+    { check = config.services.rss2email.enable;
+      path  = ["nixos" "rss2email"];
+    }
+    { check = config.services.rsyslogd.enable;
+      path  = ["nixos" "rsyslogd"];
+    }
+    { check = true;
+      path  = ["nixos" "runInMachine"];
+    }
+    { check = config.networking.rxe.enable;
+      path  = ["nixos" "rxe"];
+    }
+    { check = config.services.samba.enable;
+      path  = ["nixos" "samba"];
+    }
+    { check = config.services.sanoid.enable;
+      path  = ["nixos" "sanoid"];
+    }
+    { check = config.services.xserver.displayManager.sddm.enable;
+      paths = [
+        ["nixos" "sddm" "default"]
+        ["nixos" "sddm" "autoLogin"]
+      ];
+    }
+    { check = config.services.shiori.enable;
+      path  = ["nixos" "shiori"];
+    }
+    { check = hasPackage pkgs.signal-desktop;
+      path  = ["nixos" "signal-desktop"];
+    }
+    { check = true;
+      path  = ["nixos" "simple"];
+    }
+    { check = config.services.slurm.enableStools
+           || config.services.slurm.client.enable
+           || config.services.slurm.server.enable
+           || config.services.slurm.dbdserver.enable;
+      path = ["nixos" "slurm"];
+    }
+    { check = config.services.smokeping.enable;
+      path  = ["nixos" "smokeping"];
+    }
+    { check = config.services.snapper.configs != {};
+      path  = ["nixos" "snapper"];
+    }
+    { check = config.services.solr.enable;
+      path  = ["nixos" "solr"];
+    }
+    { check = config.services.spacecookie.enable;
+      path  = ["nixos" "spacecookie"];
+    }
+    { check = config.services.sonarr.enable;
+      path  = ["nixos" "sonarr"];
+    }
+    { check = config.services.strongswan-swanctl.enable;
+      path  = ["nixos" "strongswan-swanctl"];
+    }
+    { check = config.security.sudo.enable;
+      path  = ["nixos" "sudo"];
+    }
+    { check = true;
+      path  = ["nixos" "switchTest"];
+    }
+    { check = config.services.sympa.enable;
+      path  = ["nixos" "sympa"];
+    }
+    { check = config.services.syncthing.enable;
+      path  = ["nixos" "syncthing-init"];
+    }
+    { check = config.services.syncthing.relay.enable;
+      path  = ["nixos" "syncthing-relay"];
+    }
+    { check = true;
+      paths = [
+        ["nixos" "systemd"]
+        ["nixos" "systemd-analyze"]
+        ["nixos" "systemd-nspawn"]
+        ["nixos" "systemd-timesyncd"]
+      ];
+    }
+    { check = anyAttrs (s: s.confinement.enable) config.systemd.services;
+      path  = ["nixos" "systemd-confinement"];
+    }
+    { check = let
+        isVrf = anyAttrs (n: n.netdevConfig.Kind or "" == "vrf");
+      in config.networking.useNetworkd && isVrf config.systemd.network.netdevs;
+      path  = ["nixos" "systemd-networkd-vrf"];
+    }
+    { check = let
+        isWG = anyAttrs (n: n.netdevConfig.Kind or "" == "wireguard");
+      in config.networking.useNetworkd && isWG config.systemd.network.netdevs;
+      path  = ["nixos" "systemd-networkd-wireguard"];
+    }
+    { check = config.services.pdns-recursor.enable;
+      path  = ["nixos" "pdns-recursor"];
+    }
+    { check = config.services.taskserver.enable;
+      path  = ["nixos" "taskserver"];
+    }
+    { check = config.services.telegraf.enable;
+      path  = ["nixos" "telegraf"];
+    }
+    { check = config.services.tiddlywiki.enable;
+      path  = ["nixos" "tiddlywiki"];
+    }
+    { check = true;
+      path  = ["nixos" "timezone"];
+    }
+    { check = config.services.tinydns.enable;
+      path  = ["nixos" "tinydns"];
+    }
+    { check = config.services.tor.enable;
+      path  = ["nixos" "tor"];
+    }
+    { check = config.services.transmission.enable;
+      path  = ["nixos" "transmission"];
+    }
+    { check = config.services.trac.enable;
+      path  = ["nixos" "trac"];
+    }
+    { check = config.services.trilium-server.enable;
+      path  = ["nixos" "trilium-server"];
+    }
+    { check = config.services.trezord.enable;
+      path  = ["nixos" "trezord"];
+    }
+    { check = config.services.trickster.enable;
+      path  = ["nixos" "trickster"];
+    }
+    { check = config.services.udisks2.enable;
+      path  = ["nixos" "udisks2"];
+    }
+    { check = config.services.miniupnpd.enable
+           || hasPackage pkgs.miniupnpc_2;
+      path  = ["nixos" "upnp"];
+    }
+    { check = config.services.uwsgi.enable;
+      path  = ["nixos" "uwsgi"];
+    }
+    { check = config.services.vault.enable;
+      path  = ["nixos" "vault"];
+    }
+    { check = config.services.victoriametrics.enable;
+      path  = ["nixos" "victoriametrics"];
+    }
+    { check = config.virtualisation.virtualbox.host.enable;
+      paths = [
+        ["nixos" "virtualbox" "host-usb-permissions"]
+        ["nixos" "virtualbox" "net-hostonlyif"]
+        ["nixos" "virtualbox" "simple-cli"]
+        ["nixos" "virtualbox" "simple-gui"]
+        ["nixos" "virtualbox" "systemd-detect-virt"]
+      ];
+    }
+    { check = config.virtualisation.virtualbox.host.enable
+           && config.virtualisation.virtualbox.host.headless;
+      path  = ["nixos" "virtualbox" "headless"];
+    }
+    { check = config.networking.wireguard.enable;
+      path  = ["nixos" "wireguard"];
+    }
+    { check = with config.networking.wireguard; enable
+           && anyAttrs (i: i.generatePrivateKeyFile) interfaces;
+      path  = ["nixos" "wireguard-generated"];
+    }
+    { check = let
+        isEnabled = config.networking.wireguard.enable;
+        usesNS = iface: iface.socketNamespace != null
+              || iface.interfaceNamespace != null;
+      in isEnabled && anyAttrs usesNS config.networking.wireguard.interfaces;
+      path  = ["nixos" "wireguard-namespaces"];
+    }
+    { check = config.services.wordpress != {};
+      path  = ["nixos" "wordpress"];
+    }
+    { check = config.services.xandikos.enable;
+      path  = ["nixos" "xandikos"];
+    }
+    { check = config.services.xserver.xautolock.enable;
+      path  = ["nixos" "xautolock"];
+    }
+    { check = config.services.xserver.desktopManager.xfce.enable;
+      path  = ["nixos" "xfce"];
+    }
+    { check = config.services.xserver.windowManager.xmonad.enable;
+      path  = ["nixos" "xmonad"];
+    }
+    { check = config.services.xrdp.enable;
+      path  = ["nixos" "xrdp"];
+    }
+    { check = config.programs.xss-lock.enable;
+      path  = ["nixos" "xss-lock"];
+    }
+    { check = config.programs.yabar.enable;
+      path  = ["nixos" "yabar"];
+    }
+    { check = config.services.yggdrasil.enable;
+      path  = ["nixos" "yggdrasil"];
+    }
+    { check = elem "zfs" config.boot.supportedFilesystems
+           && !config.boot.zfs.enableUnstable;
+      path  = ["nixos" "zfs" "stable"];
+    }
+    { check = elem "zfs" config.boot.supportedFilesystems
+           && config.boot.zfs.enableUnstable;
+      path  = ["nixos" "zfs" "unstable"];
+    }
+    { check = config.programs.zsh.enable;
+      path  = ["nixos" "zsh-history"];
+    }
+    { check = config.services.zookeeper.enable;
+      path  = ["nixos" "zookeeper"];
+    }
+  ];
+
+in {
+  options.vuizvui = {
+    requiresTests = lib.mkOption {
+      type = lib.types.listOf (lib.types.listOf lib.types.str);
+      default = [];
+      example = [ ["nixos" "nat" "firewall"] ["vuizvui" "foo"] ];
+      description = ''
+        A list of attribute paths to the tests which need to succeed in order
+        to trigger a channel update for the current configuration/machine.
+
+        Every attribute path itself is a list of attribute names, which are
+        queried using <function>lib.getAttrFromPath</function>.
+      '';
+    };
+  };
+
+  config.vuizvui.requiresTests = upstreamTests;
+}
diff --git a/modules/hardware/gamecontroller.nix b/modules/hardware/gamecontroller.nix
new file mode 100644
index 00000000..5cfe9d16
--- /dev/null
+++ b/modules/hardware/gamecontroller.nix
@@ -0,0 +1,109 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  mappingType = (types.addCheck types.str (val: let
+    pattern = "[ab][0-9]+|h[0-9]+\.[0-9]+";
+  in builtins.match pattern val == [])) // {
+    name = "aI (axis), bI (button) or hI.M (hat) where I=index, M=mask";
+  };
+
+  mkAssignmentOption = example: name: description: mkOption {
+    type = types.nullOr mappingType;
+    default = null;
+    inherit example;
+    description = "Assignment for ${description}.";
+  };
+
+  mkAxisOption = mkAssignmentOption "a0";
+  mkButtonOption = mkAssignmentOption "b0";
+
+  axes = {
+    leftx = "left stick X axis";
+    lefty = "left stick Y axis";
+    rightx = "right stick X axis";
+    righty = "right stick Y axis";
+    lefttrigger = "left trigger";
+    righttrigger = "right trigger";
+  };
+
+  buttons = {
+    a = "A button (down)";
+    b = "B button (right)";
+    x = "X button (left)";
+    y = "Y button (up)";
+    back = "XBox <literal>back</literal> button";
+    guide = "XBox <literal>guide</literal> button";
+    start = "<literal>start</literal> button";
+    leftstick = "pressing the left stick";
+    rightstick = "pressing the right stick";
+    leftshoulder = "left shoulder/bumper button";
+    rightshoulder = "right shoulder/bumper button";
+    dpup = "directional pad up";
+    dpdown = "directional pad down";
+    dpleft = "directional pad left";
+    dpright = "directional pad right";
+  };
+
+  gcSubModule = { name, ... }: {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = ''
+          The name of this controller, doesn't have special meaning and is only
+          there to make it easier to dinguish various mappings.
+        '';
+      };
+
+      guid = mkOption {
+        type = types.uniq types.str;
+        default = name;
+        description = ''
+          The SDL2 GUID to uniquely identify this controller.
+
+          Use <literal>vuizvui.list-gamecontrollers</literal> to list them.
+        '';
+      };
+
+      mapping = mapAttrs mkAxisOption axes // mapAttrs mkButtonOption buttons;
+    };
+  };
+
+  mkGCLine = const (cfg: let
+    validMappings = attrNames axes ++ attrNames buttons;
+    mkMappingVal = name: let
+      val = cfg.mapping.${name} or null;
+    in if val == null then null else "${name}:${val}";
+    attrs = [ cfg.guid cfg.name "platform:Linux" ]
+         ++ remove null (map mkMappingVal validMappings);
+  in concatStringsSep "," attrs);
+
+  controllers = mapAttrsToList mkGCLine config.vuizvui.hardware.gameController;
+  controllerConfig = concatStringsSep "\n" controllers;
+
+in {
+  options.vuizvui.hardware.gameController = mkOption {
+    type = types.attrsOf (types.submodule gcSubModule);
+    default = {};
+    description = let
+      url =
+      "https://upload.wikimedia.org/wikipedia/commons/2/2c/360_controller.svg";
+    in ''
+      A mapping of the game controllers to use with SDL2 games.
+
+      The mapping is always based on the XBox reference controller, so even if
+      you don't use an XBox controller, you still have to map your keys
+      according to this layout:
+
+      <link xlink:href="${
+        "https://upload.wikimedia.org/wikipedia/commons/2/2c/360_controller.svg"
+      }"/>
+    '';
+  };
+
+  config = mkIf (config.vuizvui.hardware.gameController != {}) {
+    environment.sessionVariables.SDL_GAMECONTROLLERCONFIG = controllerConfig;
+  };
+}
diff --git a/modules/hardware/rtl8192cu/default.nix b/modules/hardware/rtl8192cu/default.nix
new file mode 100644
index 00000000..b1c037c5
--- /dev/null
+++ b/modules/hardware/rtl8192cu/default.nix
@@ -0,0 +1,50 @@
+{ config, pkgs, lib, ... }:
+
+let
+  inherit (config.boot.kernelPackages) kernel;
+
+  modBaseDir = "kernel/drivers/net/wireless";
+
+  rtl8192cu = pkgs.stdenv.mkDerivation {
+    name = "rtl8192cu-${kernel.version}";
+
+    src = pkgs.fetchFromGitHub {
+      owner = "pvaret";
+      repo = "rtl8192cu-fixes";
+      rev = "6a58e2f77d75ca9a3b80868a344ed4e2ea1816df";
+      sha256 = "130ym6ag5kgg1hdwpsfpg1i5l08lwqp1ylgjhfyhmz31h92b3h2x";
+    };
+
+    hardeningDisable = [ "stackprotector" "pic" ];
+
+    postPatch = ''
+      substituteInPlace Makefile --replace /sbin/depmod :
+    '';
+
+    makeFlags = [
+      "BUILD_KERNEL=${kernel.modDirVersion}"
+      "KSRC=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build"
+      "MODDESTDIR=$(out)/lib/modules/${kernel.modDirVersion}/${modBaseDir}/"
+    ];
+
+    preInstall = ''
+      mkdir -p "$out/lib/modules/${kernel.modDirVersion}/${modBaseDir}"
+    '';
+
+    enableParallelBuilding = true;
+  };
+
+in {
+  options.vuizvui.hardware.rtl8192cu = {
+    enable = lib.mkEnableOption "support for RTL8192CU wireless chipset";
+  };
+
+  config = lib.mkIf config.vuizvui.hardware.rtl8192cu.enable {
+    boot.extraModulePackages = [ rtl8192cu ];
+    # Note that the module is called "8192cu" so we don't blacklist the module
+    # we actually want to use. The ones we blacklist here are the modules from
+    # the mainline kernel, which unfortunately do not seem to work very well.
+    boot.blacklistedKernelModules = [ "rtl8192cu" "rtl8192c_common" "rtlwifi" ];
+    networking.enableRTL8192cFirmware = true;
+  };
+}
diff --git a/modules/hardware/t100ha/brcmfmac43340-sdio.txt b/modules/hardware/t100ha/brcmfmac43340-sdio.txt
new file mode 100644
index 00000000..db22fef6
--- /dev/null
+++ b/modules/hardware/t100ha/brcmfmac43340-sdio.txt
Binary files differdiff --git a/modules/hardware/t100ha/default.nix b/modules/hardware/t100ha/default.nix
new file mode 100644
index 00000000..0d615624
--- /dev/null
+++ b/modules/hardware/t100ha/default.nix
@@ -0,0 +1,97 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.hardware.t100ha;
+  desc = "hardware support for the ASUS T100HA convertible";
+
+in {
+  options.vuizvui.hardware.t100ha.enable = lib.mkEnableOption desc;
+
+  config = lib.mkIf cfg.enable {
+    hardware.firmware = lib.singleton (pkgs.runCommandLocal "t100ha-firmware" {
+      params = ./brcmfmac43340-sdio.txt;
+      fwpkg = pkgs.firmwareLinuxNonfree;
+      install = "install -vD -m 0644";
+    } ''
+      for fw in brcm/brcmfmac43340-sdio intel/fw_sst_22a8; do
+        $install "$fwpkg/lib/firmware/$fw.bin" "$out/lib/firmware/$fw.bin"
+      done
+      $install "$params" "$out/lib/firmware/brcm/brcmfmac43340-sdio.txt"
+    '');
+
+    boot.kernelPackages = let
+      t100haKernel = pkgs.linux_4_19.override {
+        # Missing device drivers:
+        #
+        #   808622B8 -> Intel(R) Imaging Signal Processor 2401
+        #   808622D8 -> Intel(R) Integrated Sensor Solution
+        #   HIMX2051 -> Camera Sensor Unicam hm2051
+        #   IMPJ0003 -> Impinj RFID Device (MonzaX 8K)
+        #   OVTI5670 -> Camera Sensor ov5670
+        #
+        extraConfig = ''
+          # CPU
+          MATOM y
+
+          # MMC
+          MMC y
+          MMC_BLOCK y
+          MMC_SDHCI y
+          MMC_SDHCI_ACPI y
+
+          # PMIC
+          INTEL_PMC_IPC y
+          INTEL_SOC_PMIC y
+          MFD_AXP20X y
+          MFD_AXP20X_I2C y
+
+          # Backlight
+          PWM y
+          PWM_SYSFS y
+          PWM_CRC y
+          GPIO_CRYSTAL_COVE y
+
+          # GPU
+          AGP n
+          DRM y
+          DRM_I915 m
+
+          # Thermal
+          INT3406_THERMAL m
+          INT340X_THERMAL m
+
+          # GPIO
+          PINCTRL_CHERRYVIEW y
+
+          # I2C
+          I2C_DESIGNWARE_BAYTRAIL y
+          I2C_DESIGNWARE_PLATFORM y
+
+          # HID
+          INTEL_HID_EVENT y
+
+          # MEI
+          INTEL_MEI y
+          INTEL_MEI_TXE y
+        '';
+      };
+    in pkgs.linuxPackagesFor t100haKernel;
+
+    # By default the console is rotated by 90 degrees to the right.
+    boot.kernelParams = [ "fbcon=rotate:3" ];
+    services.xserver.deviceSection = ''
+      Option "monitor-DSI1" "Monitor[0]"
+    '';
+    services.xserver.monitorSection = ''
+      Option "Rotate" "left"
+    '';
+    services.xserver.videoDriver = "intel";
+
+    # The touch screen needs to be rotated as well:
+    services.xserver.inputClassSections = lib.singleton ''
+      Identifier "touchscreen"
+      MatchProduct "SIS0457"
+      Option "TransformationMatrix" "0 -1 1 1 0 0 0 0 1"
+    '';
+  };
+}
diff --git a/modules/hardware/thinkpad.nix b/modules/hardware/thinkpad.nix
new file mode 100644
index 00000000..f94b5934
--- /dev/null
+++ b/modules/hardware/thinkpad.nix
@@ -0,0 +1,31 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.hardware.thinkpad;
+
+in
+{
+  options.vuizvui.hardware.thinkpad = {
+    enable = mkEnableOption "thinkpad support";
+  };
+
+  config = mkIf cfg.enable {
+    # read acpi stats (e.g. battery)
+    environment.systemPackages = [ pkgs.acpi ];
+
+    # for wifi
+    hardware.enableRedistributableFirmware = mkDefault true;
+
+    hardware.trackpoint = mkDefault {
+      enable = true;
+      emulateWheel = true;
+      speed = 250;
+      sensitivity = 140;
+    };
+
+    # TLP Linux Advanced Power Management
+    services.tlp.enable = mkDefault true;
+  };
+}
diff --git a/modules/module-list.nix b/modules/module-list.nix
new file mode 100644
index 00000000..d5de7026
--- /dev/null
+++ b/modules/module-list.nix
@@ -0,0 +1,36 @@
+[
+  ./core/common.nix
+  ./core/licensing.nix
+  ./core/tests.nix
+  ./core/lazy-packages.nix
+  ./hardware/gamecontroller.nix
+  ./hardware/rtl8192cu
+  ./hardware/t100ha
+  ./hardware/thinkpad.nix
+  ./programs/gnupg
+  ./programs/fish/fasd.nix
+  ./services/postfix
+  ./services/starbound.nix
+  ./services/guix.nix
+  ./system/iso.nix
+  ./system/kernel/bfq
+  ./system/kernel/rckernel.nix
+  ./system/kernel/zswap.nix
+  ./user/aszlig/profiles/base.nix
+  ./user/aszlig/profiles/managed.nix
+  ./user/aszlig/profiles/workstation
+  ./user/aszlig/programs/git
+  ./user/aszlig/programs/mpv
+  ./user/aszlig/programs/taskwarrior
+  ./user/aszlig/programs/zsh
+  ./user/aszlig/services/i3
+  ./user/aszlig/services/vlock
+  ./user/devhell/profiles/base.nix
+  ./user/devhell/profiles/packages.nix
+  ./user/devhell/profiles/services.nix
+  ./user/openlab/base.nix
+  ./user/openlab/labtops.nix
+  ./user/openlab/speedtest.nix
+  ./user/openlab/stackenblocken.nix
+  ./user/profpatsch/programs/scanning.nix
+]
diff --git a/modules/programs/fish/fasd.nix b/modules/programs/fish/fasd.nix
new file mode 100644
index 00000000..ce00d320
--- /dev/null
+++ b/modules/programs/fish/fasd.nix
@@ -0,0 +1,30 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let cfg = config.vuizvui.programs.fish.fasd;
+in
+
+{
+  options.vuizvui.programs.fish.fasd = {
+    enable = mkEnableOption "fasd integration in fish";
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.fasd ];
+
+    programs.fish = {
+      interactiveShellInit = let fasd = "${pkgs.fasd}/bin/fasd"; in ''
+        function _run_fasd -e fish_preexec
+          ${fasd} --proc (${fasd} --sanitize "$argv") > "/dev/null" 2>&1
+        end
+        function z --description "Jump to folder by usage frequency"
+          cd (fasd -d -e 'printf %s' "$argv")
+        end
+        set PATH (dirname ${fasd}) $PATH
+      '';
+    };
+
+  };
+}
diff --git a/modules/programs/gnupg/agent-wrapper.c b/modules/programs/gnupg/agent-wrapper.c
new file mode 100644
index 00000000..d9cb7a0e
--- /dev/null
+++ b/modules/programs/gnupg/agent-wrapper.c
@@ -0,0 +1,313 @@
+#define _GNU_SOURCE
+#include <dlfcn.h>
+
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <malloc.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <systemd/sd-daemon.h>
+
+#ifndef SUPERVISOR_SUPPORT
+static int main_fd = 0;
+static int scdaemon_fd = 0;
+#endif
+
+static int ssh_fd = 0;
+
+static int gather_sd_fds(void) {
+#ifdef SUPERVISOR_SUPPORT
+    if (ssh_fd == 0) {
+#else
+    if (main_fd == 0 && ssh_fd == 0 && scdaemon_fd == 0) {
+#endif
+        int num_fds;
+        char **fdmap = NULL;
+        void *libsystemd = NULL;
+        int (*_sd_listen_fds_with_names)(int, char ***);
+
+        if ((libsystemd = dlopen(LIBSYSTEMD, RTLD_LAZY)) == NULL) {
+            fprintf(stderr, "dlopen %s\n", dlerror());
+            return -4;
+        }
+
+        _sd_listen_fds_with_names =
+            dlsym(libsystemd, "sd_listen_fds_with_names");
+
+        if (_sd_listen_fds_with_names == NULL) {
+            fprintf(stderr, "dlsym %s\n", dlerror());
+            return -4;
+        }
+
+        num_fds = _sd_listen_fds_with_names(0, &fdmap);
+
+        if (num_fds <= 0) {
+            fputs("No suitable file descriptors in LISTEN_FDS.\n", stderr);
+            if (num_fds == 0)
+                return -3;
+            return -4;
+        }
+
+        if (fdmap != NULL) {
+            for (int i = 0; i < num_fds; i++) {
+                if (strncmp(fdmap[i], "ssh", 4) == 0)
+                    ssh_fd = SD_LISTEN_FDS_START + i;
+#ifndef SUPERVISOR_SUPPORT
+                else if (strncmp(fdmap[i], "main", 5) == 0)
+                    main_fd = SD_LISTEN_FDS_START + i;
+                else if (strncmp(fdmap[i], "scdaemon", 9) == 0)
+                    scdaemon_fd = SD_LISTEN_FDS_START + i;
+#endif
+                free(fdmap[i]);
+            }
+            free(fdmap);
+        }
+
+        if (dlclose(libsystemd) != 0)
+            return -1;
+    }
+
+    return 0;
+}
+
+#ifndef SUPERVISOR_SUPPORT
+
+/* Get a systemd file descriptor corresponding to the specified socket path.
+ *
+ * Return values:
+ *   -1 Socket path not a systemd socket
+ *   -2 Provided socket path is not absolute
+ *   -3 No suitable file descriptors in LISTEN_FDS
+ *   -4 Error while determining LISTEN_FDS
+ */
+static int get_sd_fd(const char *sockpath)
+{
+    int ret;
+
+    if ((ret = gather_sd_fds()) != 0)
+        return ret;
+
+    char *basename = strrchr(sockpath, '/');
+    if (basename == NULL)
+        return -2;
+    else
+        basename++;
+
+    if (strncmp(basename, "S.gpg-agent", 12) == 0)
+        return main_fd;
+    else if (strncmp(basename, "S.gpg-agent.ssh", 16) == 0)
+        return ssh_fd;
+    else if (strncmp(basename, "S.scdaemon", 11) == 0)
+        return scdaemon_fd;
+
+    return -1;
+}
+
+/* Get the systemd file descriptor for a particular sockaddr.
+ * Returns -1 if there is an error or -2 if it is an unnamed socket.
+ */
+static int get_sd_fd_sockaddr(const struct sockaddr_un *addr)
+{
+    int ret;
+
+    if (addr->sun_path == NULL || *(addr->sun_path) == 0)
+        return -2;
+
+    ret = get_sd_fd(addr->sun_path);
+
+    if (ret < 0) {
+        switch (ret) {
+            case -1:
+                fprintf(stderr, "Socket path %s is unknown.\n", addr->sun_path);
+                break;
+            case -2:
+                fprintf(stderr, "Socket path %s is not absolute.\n",
+                        addr->sun_path);
+                break;
+        }
+        errno = EADDRNOTAVAIL;
+        return -1;
+    }
+
+    return ret;
+}
+
+/* Replace the systemd-provided socket FD with the one that is used by the
+ * agent, so that we can later look it up in our accept() wrapper.
+ */
+static void record_sockfd(int sysd_fd, int redir_fd)
+{
+    if (sysd_fd == main_fd)
+        main_fd = redir_fd;
+    else if (sysd_fd == ssh_fd)
+        ssh_fd = redir_fd;
+}
+
+/* systemd is already listening on that socket, so we don't need to. */
+int listen(int sockfd, int backlog)
+{
+    return 0;
+}
+
+/* Don't unlink() the socket, because it breaks systemd socket functionality. */
+int remove(const char *pathname)
+{
+    static int (*_remove)(const char*) = NULL;
+
+    if (get_sd_fd(pathname) > 0)
+        return 0;
+
+    if (_remove == NULL)
+        _remove = dlsym(RTLD_NEXT, "remove");
+
+    return _remove(pathname);
+}
+
+/* Don't close the socket either, because we want to re-use it. */
+int close(int fd)
+{
+    static int (*_close)(int) = NULL;
+
+    if (_close == NULL)
+        _close = dlsym(RTLD_NEXT, "close");
+
+    if (fd <= 0)
+        return _close(fd);
+    else if (fd == main_fd || fd == ssh_fd || fd == scdaemon_fd)
+        return 0;
+
+    return _close(fd);
+}
+
+/* The agent should already have called socket() before and we need to close the
+ * file descriptor that the socket() call has returned and replace it with the
+ * one provided by systemd.
+ */
+int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
+{
+    int new_fd;
+
+    new_fd = get_sd_fd_sockaddr((const struct sockaddr_un *)addr);
+
+    switch (new_fd) {
+        case -1: return -1;
+        /* Unnamed socket, just pretend everything is fine */
+        case -2: return 0;
+    }
+
+    if ((new_fd = get_sd_fd_sockaddr((const struct sockaddr_un *)addr)) == -1)
+        return -1;
+
+    fprintf(stderr, "bind: Redirecting FD %d to systemd-provided FD %d.\n",
+            sockfd, new_fd);
+
+    if (dup2(new_fd, sockfd) == -1)
+        return -1;
+    else
+        record_sockfd(new_fd, sockfd);
+
+    return 0;
+}
+
+/* Avoid forking for the first time so we can properly track the agent using a
+ * systemd service (without the need to set Type="forking").
+ */
+pid_t fork(void)
+{
+    static int first_fork = 1;
+
+    static pid_t (*_fork)(void) = NULL;
+    if (_fork == NULL)
+        _fork = dlsym(RTLD_NEXT, "fork");
+
+    /* Unset the LD_PRELOAD environment variable to make sure we don't propagate
+     * it down to things like the pinentry.
+     */
+    if (unsetenv("LD_PRELOAD") == -1)
+        return -1;
+
+    if (first_fork)
+        return first_fork = 0;
+
+    return _fork();
+}
+
+#endif /* !SUPERVISOR_SUPPORT */
+
+/* Get the PID of the client connected to the given socket FD. */
+static pid_t get_socket_pid(int sockfd)
+{
+    struct ucred pcred;
+    socklen_t pcred_len = sizeof(pcred);
+
+    if (getsockopt(sockfd, SOL_SOCKET, SO_PEERCRED, &pcred, &pcred_len) == -1)
+        return -1;
+
+    return pcred.pid;
+}
+
+static pid_t last_pid = 0;
+
+/* For the pinentry to work correctly with SSH, we need to record the process ID
+ * of the process communicating with the agent. That way we can get more
+ * information about the PTS/PTY/TTY the user is on and also know whether a
+ * DISPLAY is set for that process, because we will connect the pinentry's TTY
+ * to the TTY of the process on the other end of the socket.
+ */
+int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
+{
+    int retval;
+
+    static int (*_accept)(int, struct sockaddr *, socklen_t *) = NULL;
+    if (_accept == NULL)
+        _accept = dlsym(RTLD_NEXT, "accept");
+
+    retval = _accept(sockfd, addr, addrlen);
+
+    last_pid = 0;
+
+#ifdef SUPERVISOR_SUPPORT
+    if (ssh_fd == 0) gather_sd_fds();
+#endif
+
+    if (retval != -1 && ssh_fd != 0 && sockfd == ssh_fd) {
+        pid_t client_pid = get_socket_pid(retval);
+        if (client_pid == -1) {
+            close(retval);
+            return -1;
+        }
+        last_pid = client_pid;
+        fprintf(stderr, "Socket endpoint PID for accepted socket %d is %d.\n",
+                retval, client_pid);
+    }
+
+    return retval;
+}
+
+/* Wrap the execv() that calls the pinentry program to include a special
+ * _CLIENT_PID environment variable, which contains the PID we gathered during
+ * accept(). Note that this is potentially racy if we have a lot of concurrent
+ * connections, but the worst that could happen is that we end up having a
+ * pinentry running on the wrong TTY/display.
+ */
+int execv(const char *path, char *const argv[])
+{
+    static int (*_execv)(const char *, char *const[]) = NULL;
+    if (_execv == NULL)
+        _execv = dlsym(RTLD_NEXT, "execv");
+
+    if (last_pid != 0 &&
+        strncmp(path, PINENTRY_WRAPPER, sizeof(PINENTRY_WRAPPER) + 1) == 0) {
+        char env_var[40];
+        if (snprintf(env_var, 40, "_CLIENT_PID=%d", last_pid) < 0)
+            return -1;
+        if (putenv(env_var) < 0)
+            return -1;
+    }
+
+    last_pid = 0;
+    return _execv(path, argv);
+}
diff --git a/modules/programs/gnupg/default.nix b/modules/programs/gnupg/default.nix
new file mode 100644
index 00000000..5a0ba706
--- /dev/null
+++ b/modules/programs/gnupg/default.nix
@@ -0,0 +1,191 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.programs.gnupg;
+
+  hasXdgSupport = versionAtLeast (getVersion cfg.package) "2.1.13";
+  isDefaultHome = cfg.homeDir == ".gnupg";
+
+  hasSupervisorSupport = versionAtLeast (getVersion cfg.package) "2.1.16";
+
+  sockDir = if hasXdgSupport && isDefaultHome
+            then "%t/gnupg"
+            else "%h/${cfg.homeDir}";
+  shellSockDir = if hasXdgSupport && isDefaultHome
+                 then "$XDG_RUNTIME_DIR/gnupg"
+                 else "$HOME/${cfg.homeDir}";
+
+  pinentryWrapper = pkgs.runCommandCC "pinentry-wrapper" {
+    pinentryProgram = cfg.agent.pinentry.program;
+  } ''
+    cc -Wall -std=gnu11 -DPINENTRY_PROGRAM=\"$pinentryProgram\" \
+      "${./pinentry-wrapper.c}" -o "$out"
+  '';
+
+  scdaemonRedirector = pkgs.writeScript "scdaemon-redirector" ''
+    #!${pkgs.stdenv.shell}
+    exec "${pkgs.socat}/bin/socat" - \
+      UNIX-CONNECT:"${shellSockDir}/S.scdaemon"
+  '';
+
+  agentWrapper = withSupervisor: pkgs.runCommandCC "gpg-agent-wrapper" {
+    buildInputs = with pkgs; [ pkgconfig systemd ];
+    inherit pinentryWrapper;
+  } ''
+    cc -Wall -shared -std=c11 \
+      ${optionalString withSupervisor "-DSUPERVISOR_SUPPORT=1"} \
+      -DLIBSYSTEMD=\"${pkgs.systemd.lib}/lib/libsystemd.so\" \
+      -DPINENTRY_WRAPPER=\"$pinentryWrapper\" \
+      $(pkg-config --cflags libsystemd) -ldl \
+      "${./agent-wrapper.c}" -o "$out" -fPIC
+  '';
+
+  agentSocketConfig = name: {
+    FileDescriptorName = name;
+    Service = "gpg-agent.service";
+    SocketMode = "0600";
+    DirectoryMode = "0700";
+  };
+
+in {
+  options.vuizvui.programs.gnupg = {
+    enable = mkEnableOption "support for GnuPG";
+
+    homeDir = mkOption {
+      type = types.addCheck types.str (d: builtins.substring 0 1 d != "/");
+      default = ".gnupg";
+      description = ''
+        The directory where GnuPG keeps its state files and configuration files,
+        relative to the user's home directory.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gnupg;
+      defaultText = "pkgs.gnupg";
+      example = literalExample "pkgs.gnupg21";
+      description = ''
+        The GnuPG package to use for running the agent and make available in
+        <option>environment.systemPackages</option>.
+      '';
+    };
+
+    agent = {
+      enable = mkEnableOption "support for the GnuPG agent";
+
+      pinentry.program = mkOption {
+        type = types.path;
+        default = "${pkgs.pinentry_gtk2}/bin/pinentry";
+        defaultText = "\${pkgs.pinentry_gtk2}/bin/pinentry";
+        example = literalExample "\${pkgs.pinentry_qt}/bin/pinentry";
+        description = "The pinentry program to use to ask for passphrases.";
+      };
+
+      sshSupport = mkEnableOption "GnuPG agent support for SSH";
+
+      scdaemon = {
+        enable = mkEnableOption "GnuPG agent with Smartcard daemon";
+
+        program = mkOption {
+          type = types.path;
+          default = "${cfg.package}/libexec/scdaemon";
+          defaultText = let
+            configPath = "config.vuizvui.programs.gnupg";
+          in "\${${configPath}.package}/libexec/scdaemon";
+          example = literalExample "\${pkgs.my_shiny_scdaemon}/bin/scdaemon";
+          description = "The program to use for the Smartcard daemon";
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      vuizvui.requiresTests = singleton ["vuizvui" "programs" "gnupg"];
+      environment.systemPackages = [ cfg.package ];
+    })
+    (mkIf (cfg.enable && !isDefaultHome) {
+      environment.variables.GNUPGHOME = "~/${cfg.homeDir}";
+    })
+    (mkIf (cfg.enable && cfg.agent.enable) {
+      systemd.user.services.gpg-agent = {
+        description = "GnuPG Agent";
+        environment.LD_PRELOAD = agentWrapper hasSupervisorSupport;
+        environment.GNUPGHOME = "~/${cfg.homeDir}";
+
+        serviceConfig.ExecStart = toString ([
+          "${cfg.package}/bin/gpg-agent"
+          "--pinentry-program=${pinentryWrapper}"
+          (if cfg.agent.scdaemon.enable
+           then "--scdaemon-program=${scdaemonRedirector}"
+           else "--disable-scdaemon")
+          (if hasSupervisorSupport
+           then "--supervised"
+           else "--no-detach --daemon")
+        ] ++ optional cfg.agent.sshSupport "--enable-ssh-support");
+
+        serviceConfig.ExecReload = toString [
+          "${cfg.package}/bin/gpg-connect-agent"
+          "RELOADAGENT"
+          "/bye"
+        ];
+      };
+
+      systemd.user.sockets.gpg-agent-main = {
+        wantedBy = [ "sockets.target" ];
+        description = "Main Socket For GnuPG Agent";
+        listenStreams = singleton "${sockDir}/S.gpg-agent";
+        socketConfig = let
+          sockName = if hasSupervisorSupport then "std" else "main";
+        in agentSocketConfig sockName;
+      };
+    })
+    (mkIf (cfg.enable && cfg.agent.enable && cfg.agent.scdaemon.enable) {
+      systemd.user.sockets.gnupg-scdaemon = {
+        wantedBy = [ "sockets.target" ];
+        description = "GnuPG Smartcard Daemon Socket";
+        listenStreams = singleton "${sockDir}/S.scdaemon";
+        socketConfig = {
+          FileDescriptorName = "scdaemon";
+          SocketMode = "0600";
+          DirectoryMode = "0700";
+        };
+      };
+
+      systemd.user.services.gnupg-scdaemon = {
+        description = "GnuPG Smartcard Daemon";
+        environment.LD_PRELOAD = agentWrapper false;
+        environment.GNUPGHOME = "~/${cfg.homeDir}";
+
+        serviceConfig.ExecStart = toString [
+          "${cfg.agent.scdaemon.program}"
+          "--no-detach"
+          "--daemon"
+        ];
+      };
+    })
+    (mkIf (cfg.enable && cfg.agent.enable && cfg.agent.sshSupport) {
+      environment.variables.SSH_AUTH_SOCK = "${shellSockDir}/S.gpg-agent.ssh";
+
+      systemd.user.sockets.gpg-agent-ssh = {
+        wantedBy = [ "sockets.target" ];
+        description = "SSH Socket For GnuPG Agent";
+        listenStreams = singleton "${sockDir}/S.gpg-agent.ssh";
+        socketConfig = agentSocketConfig "ssh";
+      };
+
+      assertions = singleton {
+        assertion = !config.programs.ssh.startAgent;
+        message = toString [
+          "You cannot use the GnuPG agent with SSH support in addition to the"
+          "SSH agent, please either disable"
+          "`vuizvui.programs.gpg-agent.sshSupport' or disable"
+          "`programs.ssh.startAgent'."
+        ];
+      };
+    })
+  ];
+}
diff --git a/modules/programs/gnupg/pinentry-wrapper.c b/modules/programs/gnupg/pinentry-wrapper.c
new file mode 100644
index 00000000..12710760
--- /dev/null
+++ b/modules/programs/gnupg/pinentry-wrapper.c
@@ -0,0 +1,281 @@
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+/* Get the terminal path of the given PID and FD using the /proc file system. */
+char *get_terminal(pid_t pid, int fd)
+{
+    int term, is_term;
+    ssize_t linklen;
+    char fd_path[50];
+    char term_path[100];
+    struct stat st;
+
+    if (snprintf(fd_path, 50, "/proc/%d/fd/%d", pid, fd) < 0) {
+        perror("snprintf proc fd path");
+        return NULL;
+    }
+
+    if (lstat(fd_path, &st) == -1)
+        return NULL;
+
+    if (!S_ISLNK(st.st_mode))
+        return NULL;
+
+    if ((linklen = readlink(fd_path, term_path, sizeof term_path)) == -1) {
+        perror("readlink term path");
+        return NULL;
+    }
+
+    term_path[linklen] = 0;
+
+    if ((term = open(term_path, O_RDONLY | O_NOCTTY)) == -1)
+        return NULL;
+
+    is_term = isatty(term);
+
+    if (close(term) == -1) {
+        perror("close client tty");
+        return NULL;
+    }
+
+    if (!is_term)
+        return NULL;
+
+    return strdup(term_path);
+}
+
+/* Probes FD 0, 1 and 2 for a connected terminal device and return an allocated
+ * string pointing to the filename.
+ */
+char *detect_terminal(pid_t pid)
+{
+    char *term;
+
+    for (int i = 0; i < 3; ++i) {
+        term = get_terminal(pid, i);
+        if (term == NULL)
+            continue;
+
+        return term;
+    }
+
+    return NULL;
+}
+
+/* Fetch the info from /proc/PID/environ and retorn it as an array. */
+char **fetch_environ(pid_t pid)
+{
+    char environ_path[50], **result = NULL;
+    char buf[2048], *envbuf, *environ = NULL;
+    size_t chunklen, envlen = 0;
+    int env_fd;
+
+    if (snprintf(environ_path, 50, "/proc/%d/environ", pid) < 0) {
+        perror("snprintf proc environ path");
+        return NULL;
+    }
+
+    if ((env_fd = open(environ_path, O_RDONLY)) == -1) {
+        perror("open proc environ");
+        return NULL;
+    }
+
+    while ((chunklen = read(env_fd, buf, sizeof buf)) > 0) {
+        if (environ == NULL) {
+            if ((environ = malloc(envlen + chunklen + 1)) == NULL) {
+                perror("malloc proc environ");
+                return NULL;
+            }
+        } else {
+            if ((environ = realloc(environ, envlen + chunklen + 1)) == NULL) {
+                perror("realloc proc environ");
+                free(environ);
+                return NULL;
+            }
+        }
+        memcpy(environ + envlen, buf, chunklen);
+        envlen += chunklen;
+        environ[envlen + 1] = 0;
+        if (chunklen < sizeof buf)
+            break;
+    }
+
+    if (close(env_fd) == -1) {
+        perror("close proc environ");
+        free(environ);
+        return NULL;
+    }
+
+    envbuf = environ;
+
+    if ((result = malloc(sizeof(char*))) == NULL) {
+        perror("malloc environ array");
+        free(environ);
+        return NULL;
+    }
+    result[0] = NULL;
+
+    for (int i = 0; envbuf - environ < envlen; ++i) {
+        if ((result = realloc(result, sizeof(char*) * (i + 2))) == NULL) {
+            perror("realloc environ array");
+            free(environ);
+            free(result);
+            return NULL;
+        }
+
+        result[i] = strndup(envbuf, envlen - (envbuf - environ));
+        result[i + 1] = NULL;
+        envbuf += strlen(envbuf) + 1;
+    }
+
+    free(environ);
+    return result;
+}
+
+void free_environ(char **environ)
+{
+    char **tmp = environ;
+    if (environ == NULL) return;
+    do free(*tmp);
+    while (*(++tmp) != NULL);
+    free(environ);
+    environ = NULL;
+}
+
+struct proc_info {
+    char **environ;
+    char *term;
+};
+
+/* Gather information for the given process ID, like environment or connected
+ * terminals.
+ */
+struct proc_info *open_proc_info(pid_t pid)
+{
+    struct proc_info *pi = NULL;
+
+    if ((pi = malloc(sizeof(struct proc_info *))) == NULL) {
+        perror("malloc proc_info");
+        return NULL;
+    }
+
+    pi->term = detect_terminal(pid);
+    if ((pi->environ = fetch_environ(pid)) == NULL) {
+        free(pi->term);
+        free(pi);
+        return NULL;
+    }
+
+    return pi;
+}
+
+void close_proc_info(struct proc_info *pi)
+{
+    if (pi->term != NULL) free(pi->term);
+    free_environ(pi->environ);
+    free(pi);
+}
+
+/* Fetch an environment variable from the proc_info structure similar to
+ * getenv() but for remote PIDs.
+ */
+char *proc_info_getenv(struct proc_info *pi, const char *name)
+{
+    char **tmp = pi->environ;
+    size_t namelen = strlen(name);
+    do {
+        if (strncmp(*tmp, name, namelen) == 0 &&
+            *(*tmp + namelen) == '=') {
+            return strdup(*tmp + namelen + 1);
+        }
+    } while (*(++tmp) != NULL);
+    return NULL;
+}
+
+#define MAYBE_EXPAND_ARGV(opt, value) \
+    if ((tmp = value) != NULL) { \
+        new_argv = realloc(new_argv, sizeof(char*) * (new_argc + 3)); \
+        if (new_argv == NULL) { \
+            perror("realloc new argv"); \
+            return EXIT_FAILURE; \
+        } \
+        new_argv[new_argc + 0] = "--" opt; \
+        new_argv[new_argc + 1] = tmp; \
+        new_argv[new_argc + 2] = NULL; \
+        new_argc += 2; \
+    }
+
+/* This is our program main routine whenever we get a _CLIENT_PID environment
+ * variable.
+ */
+int wrap(struct proc_info *pi, int argc, char **argv)
+{
+    char *tmp, **new_argv;
+    int new_argc = 1;
+
+    if ((new_argv = malloc(sizeof(char*) * 2)) == NULL) {
+        perror("malloc new argv");
+        return EXIT_FAILURE;
+    }
+
+    new_argv[0] = PINENTRY_PROGRAM;
+    new_argv[1] = NULL;
+
+    MAYBE_EXPAND_ARGV("display", proc_info_getenv(pi, "DISPLAY"));
+    MAYBE_EXPAND_ARGV("ttyname", strdup(pi->term));
+    MAYBE_EXPAND_ARGV("ttytype", proc_info_getenv(pi, "TERM"));
+    MAYBE_EXPAND_ARGV("lc-ctype", proc_info_getenv(pi, "LC_CTYPE"));
+    MAYBE_EXPAND_ARGV("lc-messages", proc_info_getenv(pi, "LC_MESSAGES"));
+
+    close_proc_info(pi);
+
+    /* No DISPLAY/TTY found, so use the arguments provided by the agent. */
+    if (new_argc == 1) {
+        free(new_argv);
+        new_argv = argv;
+    }
+
+    /* Make sure we don't have DISPLAY already in our environment to avoid
+     * starting a pinentry on X while the user is connected via SSH for example.
+     */
+    if (unsetenv("DISPLAY") == -1)
+        return EXIT_FAILURE;
+
+    if (execv(PINENTRY_PROGRAM, new_argv) == -1) {
+        perror("execv real pinentry");
+        return EXIT_FAILURE;
+    }
+
+    /* Not reached because the process should be substituted in execve(). */
+    return EXIT_SUCCESS;
+}
+
+int main(int argc, char **argv)
+{
+    const char *pidstr;
+    struct proc_info *pi = NULL;
+
+    if ((pidstr = getenv("_CLIENT_PID")) != NULL) {
+        if ((pi = open_proc_info(atoi(pidstr))) == NULL)
+            fprintf(stderr, "Client PID %d has vanished before we could"
+                    " retrieve /proc information.\n", atoi(pidstr));
+        else
+            return wrap(pi, argc, argv);
+    }
+
+    argv[0] = PINENTRY_PROGRAM;
+
+    if (execv(PINENTRY_PROGRAM, argv) == -1) {
+        perror("execv real pinentry");
+        return EXIT_FAILURE;
+    }
+
+    /* Not reached because the process should be substituted in execve(). */
+    return EXIT_SUCCESS;
+}
diff --git a/modules/services/guix.nix b/modules/services/guix.nix
new file mode 100644
index 00000000..287ac619
--- /dev/null
+++ b/modules/services/guix.nix
@@ -0,0 +1,106 @@
+# ATTN: this is a WIP service, use at your own risk!
+{ config, lib, pkgs, ... }:
+# https://www.gnu.org/software/guix/manual/en/html_node/Binary-Installation.html
+
+let
+  guixBinaryTar = pkgs.fetchurl {
+    url = "https://alpha.gnu.org/gnu/guix/guix-binary-0.16.0.x86_64-linux.tar.xz";
+    sha256 = "049l0zim30cd0gyly2h3jaw4cshdk78h7xdb9ac173h72i13afbj";
+  };
+
+  #*/
+  guixInstallScriptIdempotent = pkgs.writeScript "guix-install.sh" ''
+    #!/bin/sh
+    set -euo pipefail
+
+    # extract guix
+    if ! test -e /gnu; then
+      echo "INFO: installing guix"
+
+      tmp=$(mktemp -d)
+      pushd $tmp >/dev/null
+      export PATH=${pkgs.xz}/bin:$PATH
+      ${pkgs.gnutar}/bin/tar xf ${guixBinaryTar}
+      mkdir -p /var
+      cp -r ./var/guix /var
+      cp -r ./gnu /
+      popd >/dev/null
+
+      # XXX
+      # change the mtime of all compiled guile files,
+      # because tar in this script somehow changes the mtime
+      # of extracted files to the current time, and nobody knows
+      # why. If the sources are newer than the .go files, guile
+      # will try to recompile everything.
+      find /gnu/store/ -ipath "*guile*ccache*/*.go" | xargs touch -m
+    fi
+
+    # install root user profile
+    if ! test -e /root/.config/guix/current; then
+      mkdir -p /root/.config/guix
+      ln -s /var/guix/profiles/per-user/root/current-guix \
+        /root/.config/guix/current
+    fi
+
+    echo INFO: finished installing guix!
+  '';
+
+  guixBuildGroup = "guixbuilders";
+
+  guixBuildUser = id: {
+    name = "guix-build-user-${toString id}";
+    createHome = false;
+    description = "Guix build user ${toString id}";
+    extraGroups = [ guixBuildGroup ];
+    isSystemUser = true;
+  };
+
+  guixBuildUsers = numberOfUsers:
+    builtins.listToAttrs
+      (map (user: {
+        name = user.name;
+        value = user;
+      }) (builtins.genList guixBuildUser numberOfUsers));
+in
+{
+  options = {
+    vuizvui.services.guix.enable =
+      lib.mkEnableOption "the guix daemon and init /gnu/store";
+  };
+
+  config = lib.mkIf config.vuizvui.services.guix.enable {
+    users.users = guixBuildUsers 10;
+    users.groups = { "${guixBuildGroup}" = {}; };
+
+    systemd.services.guix-install = {
+      serviceConfig = {
+        ExecStart = guixInstallScriptIdempotent;
+        Type = "oneshot";
+      };
+    };
+
+    systemd.services.guix-daemon = {
+      serviceConfig = {
+        ExecStart = "/var/guix/profiles/per-user/root/current-guix/bin/guix-daemon --build-users-group=${guixBuildGroup}";
+        Environment = "GUIX_LOCPATH=/var/guix/profiles/per-user/root/guix-profix/lib/locale";
+        RemainAfterExit = true;
+        StandardOutput = "syslog";
+        StandardError = "syslog";
+        TasksMax = 8192;
+      };
+      wantedBy = [ "multi-user.target" ];
+      after = [ "guix-install.service" ];
+      wants = [ "guix-install.service" ];
+    };
+
+    environment.shellInit = ''
+      export GUIX_PROFILE="$HOME/.config/guix/current"
+      source $GUIX_PROFILE/etc/profile
+      export GUIX_LOCPATH="${pkgs.glibcLocales}/lib/locale"
+      export INFOPATH="$GUIX_PROFILE/share/info:$INFOPATH"
+
+      guix archive --authorize < \
+        /root/.config/guix/current/share/guix/ci.guix.info.pub
+    '';
+  };
+}
diff --git a/modules/services/postfix/default.nix b/modules/services/postfix/default.nix
new file mode 100644
index 00000000..8a0865b9
--- /dev/null
+++ b/modules/services/postfix/default.nix
@@ -0,0 +1,65 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.services.postfix;
+
+  mkRestriction = name: specificDescription: {
+    option.${name} = mkOption {
+      default = null;
+      type = types.nullOr (types.listOf types.str);
+      description = ''
+        A list of restrictions to apply or <option>null</option> to use the
+        built-in default value from Postfix.
+        ${specificDescription}
+      '';
+    };
+
+    config = let
+      restrictions = cfg.restrictions.${name};
+    in mkIf (restrictions != null) {
+      services.postfix.extraConfig = ''
+        smtpd_${name}_restrictions = ${concatStringsSep ", " restrictions}
+      '';
+    };
+  };
+
+  restrictions = mapAttrsToList mkRestriction {
+    client = ''
+      SMTP server access restrictions in the context of a client SMTP connection
+      request.
+    '';
+    data = ''
+      Access restrictions that the Postfix SMTP server applies in the context of
+      the SMTP DATA command.
+    '';
+    end_of_data = ''
+      Access restrictions that the Postfix SMTP server applies in the context of
+      the SMTP END-OF-DATA command.
+    '';
+    etrn = ''
+      SMTP server access restrictions in the context of a client ETRN request.
+    '';
+    helo = ''
+      Restrictions that the Postfix SMTP server applies in the context of the
+      SMTP HELO command.
+    '';
+    recipient = ''
+      Access restrictions that the Postfix SMTP server applies in the context of
+      the RCPT TO command.
+    '';
+    sender = ''
+      Restrictions that the Postfix SMTP server applies in the context of the
+      MAIL FROM command.
+    '';
+  };
+
+in {
+  options.vuizvui.services.postfix = {
+    enable = mkEnableOption "Vuizvui Postfix";
+    restrictions = fold mergeAttrs {} (catAttrs "option" restrictions);
+  };
+
+  config = mkIf cfg.enable (mkMerge (catAttrs "config" restrictions));
+}
diff --git a/modules/services/starbound.nix b/modules/services/starbound.nix
new file mode 100644
index 00000000..991ed2be
--- /dev/null
+++ b/modules/services/starbound.nix
@@ -0,0 +1,351 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.services.starbound;
+
+  mkListenerOptions = what: defaultPort: {
+    bind = mkOption {
+      type = types.str;
+      default = "::";
+      description = ''
+        Host/IP address to listen for incoming connections to the ${what}.
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = defaultPort;
+      description = ''
+        Port to listen for incoming connections to the ${what}.
+      '';
+    };
+  };
+
+  serverConfig = {
+    allowAdminCommands = cfg.adminCommands.allow;
+    allowAdminCommandsFromAnyone = cfg.adminCommands.allowFromAnyone;
+
+    allowAnonymousConnections = cfg.anonymousConnections.allow;
+    anonymousConnectionsAreAdmin = cfg.anonymousConnections.adminPrivileges;
+
+    serverUsers = mapAttrs (user: attrs: {
+      inherit (attrs) admin password;
+    }) cfg.users;
+
+    inherit (cfg)
+      allowAssetsMismatch maxPlayers maxTeamSize serverName serverFidelity;
+
+    clearPlayerFiles = false;
+    clearUniverseFiles = false;
+
+    safeScripts = cfg.safeScripts.enable;
+    scriptInstructionLimit = cfg.safeScripts.instructionLimit;
+    scriptInstructionMeasureInterval =
+      cfg.safeScripts.instructionMeasureInterval;
+    scriptProfilingEnabled = cfg.safeScripts.profiling.enable;
+    scriptRecursionLimit = cfg.safeScripts.recursionLimit;
+
+    gameServerBind = cfg.bind;
+    gameServerPort = cfg.port;
+
+    bannedIPs = cfg.bannedIPs;
+    bannedUuids = cfg.bannedUUIDs;
+
+    runRconServer = cfg.rconServer.enable;
+    rconServerBind = cfg.rconServer.bind;
+    rconServerPort = cfg.rconServer.port;
+    rconServerPassword = cfg.rconServer.password;
+    rconServerTimeout = cfg.rconServer.timeout;
+
+    runQueryServer = cfg.queryServer.enable;
+    queryServerBind = cfg.queryServer.bind;
+    queryServerPort = cfg.queryServer.port;
+  } // cfg.extraConfig;
+
+  bootConfig = pkgs.writeText "sbinit.config" (builtins.toJSON {
+    logFileBackups = 0;
+    storageDirectory = cfg.dataDir;
+    assetDirectories = singleton (cfg.package.assets);
+    defaultConfiguration = serverConfig;
+  });
+
+  # Traverse a given path with ../ until we get to the root directory (/).
+  gotoRoot = p: concatStringsSep "/" (map (const "..") (splitString "/" p));
+
+in {
+  options.vuizvui.services.starbound = {
+    enable = mkEnableOption "Starbound game server";
+
+    adminCommands = {
+      allow = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow admin commands in general.
+        '';
+        # XXX: Make this dependant on whether an account is defined with enabled
+        # admin.
+      };
+
+      allowFromAnyone = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow anyone, even anonymous users to use admin commands.
+        '';
+        # XXX: Check whether this is true!
+      };
+    };
+
+    anonymousConnections = {
+      allow = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow anonymous connections to the server.
+
+          Set this to <literal>false</literal> and use
+          <option>serverUsers</option> to only allow specific accounts to
+          connect.
+        '';
+      };
+
+      adminPrivileges = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether all anonymous connections have administrative privileges.
+        '';
+      };
+    };
+
+    users = mkOption {
+      type = types.attrsOf (types.submodule {
+        options.admin = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether this user has admin privileges.
+          '';
+        };
+        options.password = mkOption {
+          type = types.str;
+          example = "supersecure";
+          description = ''
+            The password for the user.
+          '';
+        };
+      });
+      default = {};
+      description = ''
+        User accounts to allow connection to the Starbound server.
+      '';
+    };
+
+    allowAssetsMismatch = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Check whether the assets on the client match the ones from the server
+        and deny connection if they don't match.
+      '';
+    };
+
+    bannedIPs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        IP addresses disallowed for connection to the server.
+      '';
+    };
+
+    bannedUUIDs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        User IDs disallowed for connection to the server.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/starbound";
+      description = ''
+        The directory where Starbound stores its universe/player files.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.vuizvui.games.humblebundle.starbound;
+      defaultText = "pkgs.vuizvui.games.humblebundle.starbound";
+      description = ''
+        The starbound package to use for running this game server.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        Extra configuration options to add to the server config.
+      '';
+    };
+
+    rconServer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run an RCON server which allows to run administrative
+          commands on this game server instance.
+
+          See the <link xlink:href="${
+            "https://developer.valvesoftware.com/wiki/Source_RCON_Protocol"
+          }">RCON protocol documentation</link> for more information about this.
+        '';
+      };
+
+      password = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          The password needed to authorize with the RCON server.
+        '';
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 1000;
+        # XXX: Find out what this timeout is for and whether it's in seconds.
+        description = ''
+          After how many seconds the RCON server drops the connection.
+        '';
+      };
+    } // mkListenerOptions "RCON server" 21026;
+
+    queryServer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run a query server that shows information such as currently
+          connected players.
+        '';
+      };
+    } // mkListenerOptions "query server" 21025;
+
+    safeScripts = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable certain limitations of LUA scripts.
+        '';
+      };
+
+      instructionLimit = mkOption {
+        type = types.int;
+        default = 10000000;
+        description = ''
+          The maximum amount of instructions a LUA function can have.
+        '';
+      };
+
+      instructionMeasureInterval = mkOption {
+        type = types.int;
+        default = 10000;
+        description = ''
+          The amount of milliseconds to wait between consecutive checks of the
+          <option>instructionLimit</option> on LUA scripts.
+        '';
+      };
+
+      recursionLimit = mkOption {
+        type = types.int;
+        default = 100;
+        description = ''
+          Maximum depth of recursion for LUA scripts.
+        '';
+      };
+
+      profiling.enable = mkEnableOption "LUA script profiling";
+    };
+
+    serverName = mkOption {
+      type = types.str;
+      default = "A Starbound Server";
+      example = "My shiny Starbound Server";
+      description = ''
+        A short description or name of the Starbound server to run.
+      '';
+    };
+
+    serverFidelity = mkOption {
+      type = types.enum [ "automatic" "minimum" "low" "medium" "high" ];
+      default = "automatic";
+      example = "high";
+      description = ''
+        The fidelity profile to use for this server as defined in
+        <filename>worldserver.config</filename> inside the packed assets.
+
+        If this is set to <literal>automatic</literal> the server will
+        automatically switch between these profiles.
+      '';
+    };
+
+    maxPlayers = mkOption {
+      type = types.int;
+      default = 8;
+      description = ''
+        Maximum amount of players to allow concurrently.
+      '';
+    };
+
+    maxTeamSize = mkOption {
+      type = types.int;
+      default = 4;
+      description = ''
+        Maximum amount of players to allow within a party.
+      '';
+    };
+  } // mkListenerOptions "game server" 21025;
+
+  config = mkIf cfg.enable {
+    users.groups.starbound = {
+      gid = config.ids.gids.starbound;
+    };
+
+    users.users.starbound = {
+      uid = config.ids.uids.starbound;
+      description = "Starbound Game Server User";
+      group = "starbound";
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    systemd.services.starbound = {
+      description = "Starbound Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "fs.target" ];
+
+      serviceConfig = {
+        User = "starbound";
+        Group = "starbound";
+        PrivateTmp = true;
+
+        KillSignal = "SIGINT";
+
+        ExecStart = toString [
+          "${cfg.package}/bin/starbound-server"
+          "-bootconfig \"${bootConfig}\""
+          # Workaround to disable logging to file
+          "-logfile \"${gotoRoot cfg.dataDir}/dev/null\""
+          "-verbose"
+        ];
+      };
+    };
+  };
+}
diff --git a/modules/system/iso.nix b/modules/system/iso.nix
new file mode 100644
index 00000000..893a56e9
--- /dev/null
+++ b/modules/system/iso.nix
@@ -0,0 +1,12 @@
+{ lib, ... }:
+
+{
+  options.vuizvui.createISO = lib.mkOption {
+    default = false;
+    example = true;
+    type = lib.types.bool;
+    description = ''
+      Whether to build an ISO image out of this machine configuration on Hydra.
+    '';
+  };
+}
diff --git a/modules/system/kernel/bfq/bfq-by-default-4.15.patch b/modules/system/kernel/bfq/bfq-by-default-4.15.patch
new file mode 100644
index 00000000..8a4666ca
--- /dev/null
+++ b/modules/system/kernel/bfq/bfq-by-default-4.15.patch
@@ -0,0 +1,13 @@
+diff --git a/block/elevator.c b/block/elevator.c
+index 7bda083d5968..8dddfaf725fc 100644
+--- a/block/elevator.c
++++ b/block/elevator.c
+@@ -246,7 +246,7 @@ int elevator_init(struct request_queue *q, char *name)
+ 		 */
+ 		if (q->mq_ops) {
+ 			if (q->nr_hw_queues == 1)
+-				e = elevator_get(q, "mq-deadline", false);
++				e = elevator_get(q, "bfq", false);
+ 			if (!e)
+ 				return 0;
+ 		} else
diff --git a/modules/system/kernel/bfq/bfq-by-default-4.18.patch b/modules/system/kernel/bfq/bfq-by-default-4.18.patch
new file mode 100644
index 00000000..5ece12cb
--- /dev/null
+++ b/modules/system/kernel/bfq/bfq-by-default-4.18.patch
@@ -0,0 +1,13 @@
+diff --git a/block/elevator.c b/block/elevator.c
+index fa828b5bfd4b..cdd582a18e50 100644
+--- a/block/elevator.c
++++ b/block/elevator.c
+@@ -994,7 +994,7 @@ int elevator_init_mq(struct request_queue *q)
+ 	if (unlikely(q->elevator))
+ 		goto out_unlock;
+ 
+-	e = elevator_get(q, "mq-deadline", false);
++	e = elevator_get(q, "bfq", false);
+ 	if (!e)
+ 		goto out_unlock;
+ 
diff --git a/modules/system/kernel/bfq/bfq-by-default-5.4.patch b/modules/system/kernel/bfq/bfq-by-default-5.4.patch
new file mode 100644
index 00000000..7f06ec41
--- /dev/null
+++ b/modules/system/kernel/bfq/bfq-by-default-5.4.patch
@@ -0,0 +1,13 @@
+diff --git a/block/elevator.c b/block/elevator.c
+index 4eab3d70e880..7ea8f9d34e86 100644
+--- a/block/elevator.c
++++ b/block/elevator.c
+@@ -631,7 +631,7 @@ static struct elevator_type *elevator_get_default(struct request_queue *q)
+ 	if (q->nr_hw_queues != 1)
+ 		return NULL;
+ 
+-	return elevator_get(q, "mq-deadline", false);
++	return elevator_get(q, "bfq", false);
+ }
+ 
+ /*
diff --git a/modules/system/kernel/bfq/bfq-by-default.patch b/modules/system/kernel/bfq/bfq-by-default.patch
new file mode 100644
index 00000000..c6ee0492
--- /dev/null
+++ b/modules/system/kernel/bfq/bfq-by-default.patch
@@ -0,0 +1,13 @@
+diff --git a/block/elevator.c b/block/elevator.c
+index dac99fbfc273..fbcdba53a3aa 100644
+--- a/block/elevator.c
++++ b/block/elevator.c
+@@ -229,7 +229,7 @@ int elevator_init(struct request_queue *q, char *name)
+ 		 */
+ 		if (q->mq_ops) {
+ 			if (q->nr_hw_queues == 1)
+-				e = elevator_get("mq-deadline", false);
++				e = elevator_get("bfq", false);
+ 			if (!e)
+ 				return 0;
+ 		} else
diff --git a/modules/system/kernel/bfq/default.nix b/modules/system/kernel/bfq/default.nix
new file mode 100644
index 00000000..a4b593ee
--- /dev/null
+++ b/modules/system/kernel/bfq/default.nix
@@ -0,0 +1,36 @@
+{ config, lib, ... }:
+
+let
+  inherit (config.boot.kernelPackages.kernel) version;
+  inherit (lib) optionalString versionAtLeast versionOlder;
+in {
+  options.vuizvui.system.kernel.bfq = {
+    enable = lib.mkEnableOption "Enable the BFQ scheduler by default";
+  };
+
+  config = lib.mkIf config.vuizvui.system.kernel.bfq.enable {
+    boot.kernelPatches = lib.singleton {
+      name = "bfq";
+      patch =
+        if      versionAtLeast version "5.4"  then ./bfq-by-default-5.4.patch
+        else if versionAtLeast version "4.18" then ./bfq-by-default-4.18.patch
+        else if versionAtLeast version "4.15" then ./bfq-by-default-4.15.patch
+        else ./bfq-by-default.patch;
+      extraConfig = ''
+        SCSI_MQ_DEFAULT? y
+        DM_MQ_DEFAULT? y
+        IOSCHED_BFQ y
+        BFQ_GROUP_IOSCHED y
+      '';
+    };
+
+    vuizvui.requiresTests = lib.singleton ["vuizvui" "system" "kernel" "bfq"];
+
+    assertions = lib.singleton {
+      assertion = versionAtLeast version "4.12";
+
+      message = "The BFQ scheduler in conjunction with blk-mq requires "
+              + "at least kernel 4.12.";
+    };
+  };
+}
diff --git a/modules/system/kernel/rckernel.nix b/modules/system/kernel/rckernel.nix
new file mode 100644
index 00000000..a3ccf907
--- /dev/null
+++ b/modules/system/kernel/rckernel.nix
@@ -0,0 +1,23 @@
+{ config, pkgs, lib, ... }:
+
+{
+  options.vuizvui.system.kernel.useBleedingEdge = lib.mkOption {
+    type = lib.types.bool;
+    default = false;
+    description = ''
+      Whether to always use the latest kernel, even if it's still a release
+      canidate version.
+    '';
+  };
+
+  config = lib.mkIf config.vuizvui.system.kernel.useBleedingEdge {
+    boot.kernelPackages = let
+      inherit (lib) take splitString replaceStrings;
+      inherit (pkgs) linux_latest linux_testing;
+      dotizeVer = replaceStrings ["-"] ["."];
+      trimVer = ver: take 2 (splitString "." (dotizeVer ver));
+      tooOld = trimVer linux_latest.version == trimVer linux_testing.version;
+      kernel = if tooOld then linux_latest else linux_testing;
+    in pkgs.linuxPackagesFor kernel;
+  };
+}
diff --git a/modules/system/kernel/zswap.nix b/modules/system/kernel/zswap.nix
new file mode 100644
index 00000000..3b8d4bca
--- /dev/null
+++ b/modules/system/kernel/zswap.nix
@@ -0,0 +1,37 @@
+{ config, pkgs, lib, ... }:
+
+let
+  kernelVersion = config.boot.kernelPackages.kernel.version;
+  hasZstd = lib.versionAtLeast kernelVersion "4.18";
+in {
+  options.vuizvui.system.kernel.zswap.enable = lib.mkOption {
+    type = lib.types.bool;
+    default = false;
+    description = ''
+      Whether to enable support for zswap with <literal>z3fold</literal> for
+      pooling and <literal>zstd</literal> for compression, if available
+      (otherwise it falls back to <literal>lzo</literal>).
+
+      Zswap is a compressed cache for swap pages, which is especially useful
+      for machines with limited RAM.
+    '';
+  };
+
+  config = lib.mkIf config.vuizvui.system.kernel.zswap.enable {
+    boot.kernelPatches = lib.singleton {
+      name = "zswap-config";
+      patch = null;
+      extraConfig = ''
+        CRYPTO_${if hasZstd then "ZSTD" else "LZO"} y
+        ZSWAP y
+        Z3FOLD y
+      '';
+    };
+
+    boot.kernelParams = [
+      "zswap.enabled=1"
+      "zswap.zpool=z3fold"
+      "zswap.compressor=${if hasZstd then "zstd" else "lzo"}"
+    ];
+  };
+}
diff --git a/modules/user/aszlig/profiles/base.nix b/modules/user/aszlig/profiles/base.nix
new file mode 100644
index 00000000..ff3f3f9f
--- /dev/null
+++ b/modules/user/aszlig/profiles/base.nix
@@ -0,0 +1,97 @@
+{ config, pkgs, unfreePkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.aszlig.profiles.base;
+
+in {
+  options.vuizvui.user.aszlig.profiles.base = {
+    enable = lib.mkEnableOption "Base profile for aszlig";
+  };
+
+  config = lib.mkIf cfg.enable {
+    nix = {
+      useSandbox = true;
+      readOnlyStore = true;
+      buildCores = 0;
+      extraOptions = ''
+        auto-optimise-store = true
+      '';
+    };
+
+    boot.loader.grub = {
+      enable = true;
+      version = 2;
+    };
+
+    hardware.cpu.intel.updateMicrocode = true;
+
+    users.defaultUserShell = "/var/run/current-system/sw/bin/zsh";
+
+    networking.wireless.enable = false;
+    networking.firewall.enable = false;
+    networking.useNetworkd = true;
+    networking.useDHCP = false;
+
+    console.keyMap = "dvorak";
+    console.font = "lat9w-16";
+
+    programs.ssh.startAgent = false;
+    programs.ssh.extraConfig = ''
+      ServerAliveInterval 60
+    '';
+
+    vuizvui.user.aszlig.programs.zsh.enable = true;
+    vuizvui.enableGlobalNixpkgsConfig = true;
+
+    services.nixosManual.showManual = false;
+
+    services.journald.extraConfig = ''
+      MaxRetentionSec=3month
+    '';
+
+    services.openssh.passwordAuthentication = lib.mkDefault false;
+    services.openssh.permitRootLogin = lib.mkDefault "no";
+    services.openssh.challengeResponseAuthentication = lib.mkDefault false;
+
+    environment.systemPackages = with pkgs; [
+      binutils
+      cacert
+      file
+      htop
+      iotop
+      moreutils
+      psmisc
+      unfreePkgs.unrar
+      unzip
+      vlock
+      vuizvui.aszlig.vim
+      wget
+      xz
+    ];
+
+    nixpkgs.config = {
+      pulseaudio = true;
+      allowBroken = true;
+    };
+
+    nixpkgs.overlays = lib.singleton (lib.const (super: {
+      beets = super.beets.override {
+        enableAlternatives = true;
+      };
+      netrw = super.netrw.override {
+        checksumType = "mhash";
+      };
+      nix = super.nixUnstable;
+      uqm = super.uqm.override {
+        use3DOVideos = true;
+        useRemixPacks = true;
+      };
+      w3m = super.w3m.override {
+        graphicsSupport = true;
+      };
+    }));
+
+    system.fsPackages = with pkgs; [ sshfsFuse ];
+    time.timeZone = "Europe/Berlin";
+  };
+}
diff --git a/modules/user/aszlig/profiles/managed.nix b/modules/user/aszlig/profiles/managed.nix
new file mode 100644
index 00000000..92566e04
--- /dev/null
+++ b/modules/user/aszlig/profiles/managed.nix
@@ -0,0 +1,115 @@
+{ pkgs, unfreePkgs, unfreeAndNonDistributablePkgs, config, lib, ... }:
+
+let
+  inherit (lib) mkIf mkEnableOption mkOption;
+  cfg = config.vuizvui.user.aszlig.profiles.managed;
+  inherit (cfg) mainUser;
+
+in {
+  options.vuizvui.user.aszlig.profiles.managed = {
+    enable = mkEnableOption "common profile for aszlig's managed machines";
+
+    mainUser = mkOption {
+      example = "foobar";
+      description = ''
+        Main user account of the managed system.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    vuizvui.system.kernel.bfq.enable = true;
+
+    boot.cleanTmpDir = true;
+    boot.loader.systemd-boot.enable = true;
+    boot.loader.efi.canTouchEfiVariables = true;
+
+    environment.systemPackages = [
+      pkgs.file
+      pkgs.gajim
+      pkgs.gimp
+      pkgs.git
+      pkgs.htop
+      pkgs.inkscape
+      (unfreeAndNonDistributablePkgs.kdeApplications.ark.override {
+        unfreeEnableUnrar = true;
+        inherit (unfreePkgs) unrar;
+      })
+      pkgs.kdeApplications.gwenview
+      pkgs.kdeApplications.kaddressbook
+      pkgs.kdeApplications.kate
+      pkgs.kdeApplications.kdepim-addons
+      pkgs.kdeApplications.kleopatra
+      pkgs.kdeApplications.kmail
+      pkgs.kdeApplications.kontact
+      pkgs.kdeApplications.korganizer
+      pkgs.kdeApplications.okular
+      pkgs.libreoffice
+      pkgs.mpv
+      pkgs.skanlite
+      pkgs.thunderbird
+      pkgs.vuizvui.aszlig.vim
+      pkgs.wine
+      pkgs.youtubeDL
+      unfreeAndNonDistributablePkgs.skype
+    ];
+
+    i18n.consoleUseXkbConfig = true;
+
+    # Printing for the most common printers among the managed machines.
+    services.printing.enable = true;
+    services.printing.drivers = [
+      pkgs.gutenprint
+      unfreeAndNonDistributablePkgs.hplipWithPlugin
+    ];
+
+    # For MTP and other stuff.
+    services.gvfs.enable = true;
+
+    # Plasma desktop with German keyboard layout.
+    services.xserver.enable = true;
+    services.xserver.layout = "de";
+    services.xserver.xkbOptions = lib.mkOverride 900 "eurosign:e";
+    services.xserver.displayManager.sddm.enable = true;
+    services.xserver.displayManager.defaultSession = "plasma5";
+    services.xserver.desktopManager.plasma5.enable = true;
+
+    # And also most common scanners are also HP ones.
+    hardware.sane.enable = true;
+    hardware.sane.extraBackends = [
+      unfreeAndNonDistributablePkgs.hplipWithPlugin
+    ];
+
+    hardware.opengl.s3tcSupport = true;
+    hardware.opengl.driSupport32Bit = true;
+    hardware.pulseaudio.enable = true;
+    hardware.pulseaudio.package = pkgs.pulseaudioFull;
+    sound.enable = true;
+
+    networking.firewall.enable = false;
+    networking.networkmanager.enable = true;
+
+    nix.autoOptimiseStore = true;
+    nix.buildCores = 0;
+    nix.readOnlyStore = true;
+    nix.useSandbox = true;
+
+    nixpkgs.config.chromium.enablePepperFlash = true;
+    nixpkgs.config.pulseaudio = true;
+
+    programs.bash.enableCompletion = true;
+
+    services.tlp.enable = true;
+
+    time.timeZone = "Europe/Berlin";
+
+    users.users.${mainUser} = {
+      isNormalUser = true;
+      uid = 1000;
+      extraGroups = [ "networkmanager" "scanner" "video" "wheel" ];
+    };
+
+    vuizvui.enableGlobalNixpkgsConfig = true;
+    vuizvui.system.kernel.zswap.enable = true;
+  };
+}
diff --git a/modules/user/aszlig/profiles/workstation/default.nix b/modules/user/aszlig/profiles/workstation/default.nix
new file mode 100644
index 00000000..4e4ea876
--- /dev/null
+++ b/modules/user/aszlig/profiles/workstation/default.nix
@@ -0,0 +1,186 @@
+{ pkgs, config, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.aszlig.profiles.workstation;
+  inherit (config.services.xserver) xrandrHeads;
+in {
+  options.vuizvui.user.aszlig.profiles.workstation = {
+    enable = lib.mkEnableOption "Workstation profile for aszlig";
+  };
+
+  config = lib.mkIf cfg.enable {
+    vuizvui.user.aszlig.profiles.base.enable = true;
+
+    boot.kernelParams = [ "panic=1800" ];
+    boot.cleanTmpDir = true;
+
+    environment.systemPackages = with lib; let
+      mkRandrConf = acc: rcfg: acc ++ singleton {
+        name = rcfg.output;
+        value = "--output ${lib.escapeShellArg rcfg.output} --preferred"
+              + optionalString rcfg.primary " --primary"
+              + optionalString (acc != []) " --right-of '${(head acc).name}'";
+      };
+      randrConf = map (getAttr "value") (foldl mkRandrConf [] xrandrHeads);
+    in singleton (pkgs.writeScriptBin "xreset" ''
+      #!${pkgs.stdenv.shell}
+      ${pkgs.xorg.xrandr}/bin/xrandr ${concatStringsSep " " randrConf}
+    '') ++ import ./packages.nix pkgs ++ [
+      (pkgs.vuizvui.aszlig.psi.override {
+        jid = "aszlig@aszlig.net";
+        resource = config.networking.hostName;
+      })
+    ];
+
+    environment.pathsToLink = lib.singleton "/share/chromium/extensions";
+
+    # The default theme hurts my eyes.
+    environment.variables.GTK_THEME = "Adwaita:dark";
+
+    vuizvui.lazyPackages = import ./lazy-packages.nix pkgs;
+
+    sound.enable = true;
+
+    hardware = {
+      pulseaudio.enable = true;
+      pulseaudio.package = pkgs.pulseaudioFull;
+      opengl = {
+        driSupport32Bit = true;
+        s3tcSupport = true;
+      };
+    };
+
+    fonts = {
+      enableFontDir = true;
+      enableGhostscriptFonts = true;
+      fonts = [
+        pkgs.dosemu_fonts
+        pkgs.liberation_ttf
+      ];
+    };
+
+    vuizvui.user.aszlig.services.i3.enable = true;
+    vuizvui.user.aszlig.services.vlock.enable = true;
+    vuizvui.user.aszlig.services.vlock.user = "aszlig";
+
+    vuizvui.user.aszlig.programs.mpv.enable = true;
+    vuizvui.user.aszlig.programs.taskwarrior.enable = true;
+
+    vuizvui.user.aszlig.programs.git.enable = true;
+    vuizvui.user.aszlig.programs.git.config = {
+      color.ui = "auto";
+      merge.tool = "vimdiff3";
+      user.email = "aszlig@nix.build";
+      user.name = "aszlig";
+      user.signingkey = "DD526BC7767DBA2816C095E5684089CE67EBB691";
+      gpg.program = "${pkgs.gnupg}/bin/gpg";
+      push.default = "current";
+      tar."tar.xz".command = "${pkgs.xz}/bin/xz -c";
+      rebase.autosquash = true;
+      rerere.enabled = true;
+      rerere.autoupdate = true;
+      commit.gpgsign = true;
+
+      alias.backport = let
+        release = "14.04";
+        message = "Merge release ${release} into backports.";
+      in "!git fetch upstream release-${release} &&"
+       + " git merge -m \"${message}\" --log FETCH_HEAD";
+    };
+
+    vuizvui.hardware.gameController."03000000ff1100004133000010010000" = {
+      name = "PS2 Controller";
+      mapping = {
+        a = "b2";
+        b = "b1";
+        x = "b3";
+        y = "b0";
+        back = "b8";
+        start = "b9";
+        leftshoulder = "b6";
+        rightshoulder = "b7";
+        leftstick = "b10";
+        rightstick = "b11";
+        leftx = "a0";
+        lefty = "a1";
+        rightx = "a3";
+        righty = "a2";
+        lefttrigger = "b4";
+        righttrigger = "b5";
+        dpup = "h0.1";
+        dpleft = "h0.8";
+        dpdown = "h0.4";
+        dpright = "h0.2";
+      };
+    };
+
+    vuizvui.programs.gnupg.enable = true;
+    vuizvui.programs.gnupg.agent.enable = true;
+    vuizvui.programs.gnupg.agent.sshSupport = true;
+    vuizvui.programs.gnupg.agent.scdaemon.enable = true;
+
+    vuizvui.system.kernel.zswap.enable = true;
+
+    location.latitude = 48.4284;
+    location.longitude = 10.866;
+
+    services = {
+      openssh.enable = true;
+
+      xfs.enable = false;
+
+      gpm = {
+        enable = true;
+        protocol = "exps2";
+      };
+
+      printing.enable = true;
+      printing.drivers = [ pkgs.gutenprint pkgs.hplip ];
+
+      pcscd.enable = true;
+      pcscd.plugins = [ pkgs.ccid pkgs.pcsc-cyberjack ];
+
+      udev.extraRules = ''
+        # aXbo S.P.A.C.
+        SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \
+          ATTRS{serial}=="0001", OWNER="aszlig", SYMLINK+="axbo"
+        # Enttec DMX device
+        SUBSYSTEM=="usb*|tty", ACTION=="add|change", ATTRS{idVendor}=="0403", \
+          ATTRS{idProduct}=="6001", OWNER="aszlig"
+      '';
+
+      redshift = {
+        enable = true;
+        temperature.day = 5500;
+        temperature.night = 3500;
+      };
+
+      xserver = {
+        enable = true;
+        layout = "dvorak";
+
+        displayManager.lightdm.enable = true;
+        displayManager.defaultSession = "none+i3";
+        displayManager.sessionCommands = ''
+          ${pkgs.xorg.xrdb}/bin/xrdb "${pkgs.writeText "xrdb.config" ''
+            XTerm*font:                vga
+            XTerm*saveLines:           10000
+            XTerm*bellIsUrgent:        true
+            XTerm*background:          black
+            XTerm*foreground:          grey
+
+            XTerm*backarrowKeyIsErase: true
+            XTerm*ptyInitialErase:     true
+          ''}"
+        '';
+      };
+    };
+
+    users.users.aszlig = {
+      uid = 1000;
+      isNormalUser = true;
+      description = "aszlig";
+      extraGroups = [ "wheel" "video" ];
+    };
+  };
+}
diff --git a/modules/user/aszlig/profiles/workstation/lazy-packages.nix b/modules/user/aszlig/profiles/workstation/lazy-packages.nix
new file mode 100644
index 00000000..06b6d3f4
--- /dev/null
+++ b/modules/user/aszlig/profiles/workstation/lazy-packages.nix
@@ -0,0 +1,25 @@
+pkgs: with pkgs; [
+  vuizvui.aszlig.aacolorize
+  aqbanking
+  erlang
+  fbida
+  gimp
+  gwenhywfar
+  graphviz
+  haskellPackages.cabal2nix
+  haskellPackages.cabal-install
+  haskellPackages.hlint
+  haxe
+  libchipcard
+  lftp
+  mp3info
+  mpg321
+  mumble
+  neko
+  nixpkgs-lint
+  picard
+  rtmpdump
+  rtorrent
+  uqm
+  vuizvui.aszlig.xournal
+]
diff --git a/modules/user/aszlig/profiles/workstation/packages.nix b/modules/user/aszlig/profiles/workstation/packages.nix
new file mode 100644
index 00000000..f0646207
--- /dev/null
+++ b/modules/user/aszlig/profiles/workstation/packages.nix
@@ -0,0 +1,85 @@
+pkgs: with pkgs; [
+  abook
+  acpi
+  apg
+  ascii
+  aspellDicts.de
+  aspellDicts.en
+  vuizvui.aszlig.axbo
+  bc
+  beets
+  chromium
+  dash
+  dos2unix
+  fd
+  feh
+  ffmpeg
+  figlet
+  firefox
+  flac
+  gdb
+  ghostscript
+  vuizvui.aszlig.git-detach
+  glxinfo
+  gnumake
+  gnupg1compat
+  vuizvui.aszlig.gopass
+  hexedit
+  hledger
+  hledger-ui
+  hledger-web
+  i3
+  i3lock
+  imagemagick
+  jwhois
+  jq
+  keychain
+  ltrace
+  man-pages
+  mmv
+  mosh
+  mtr
+  mutt
+  ncdu
+  netrw
+  nix-prefetch-scripts
+  nixops
+  nmap
+  openssh
+  openssl
+  p7zip
+  pavucontrol
+  posix_man_pages
+  pulseaudioLight
+  vuizvui.aszlig.pvolctrl
+  pv
+  python
+  python3
+  pythonPackages.hetzner
+  pythonPackages.pep8
+  pythonPackages.polib
+  radare2
+  rlwrap
+  rsync
+  samplicator
+  screen
+  scrot
+  socat
+  sox
+  sqlite
+  stdmanpages
+  strace
+  surfraw
+  vuizvui.taalo-build
+  telnet
+  unzip
+  valgrind
+  vbindiff
+  vorbisTools
+  w3m
+  wcc
+  wireshark
+  xorg.xhost
+  youtubeDL
+  zathura
+]
diff --git a/modules/user/aszlig/programs/git/default.nix b/modules/user/aszlig/programs/git/default.nix
new file mode 100644
index 00000000..3cfdc742
--- /dev/null
+++ b/modules/user/aszlig/programs/git/default.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.user.aszlig.programs.git;
+
+  genConf = attrs: let
+    escStr = s: "\"${escape [ "\"" "\\" ] s}\"";
+    mkVal = v: if isBool v && v  then "true"
+          else if isBool v && !v then "false"
+          else escStr (toString v);
+    mkLine = key: val: "${key} = ${mkVal val}";
+
+    filterNull = filterAttrs (_: v: !(isNull v));
+
+    mkSection = sect: subsect: vals: ''
+      [${sect}${optionalString (subsect != null) " ${escStr subsect}"}]
+      ${concatStringsSep "\n" (mapAttrsToList mkLine (filterNull vals))}
+    '';
+
+    mkConf = sect: content: let
+      subs = filterAttrs (_: isAttrs) content;
+      nonSubs = filterAttrs (_: s: !isAttrs s) content;
+      hasPlain = (attrNames nonSubs) != [];
+      plainSects = singleton (mkSection sect null nonSubs);
+    in mapAttrsToList (mkSection sect) subs ++ optional hasPlain plainSects;
+
+    text = concatStringsSep "\n" (flatten (mapAttrsToList mkConf attrs));
+  in pkgs.writeText "gitconfig" text;
+
+  gitPatched = overrideDerivation pkgs.gitFull (git: {
+    makeFlags = let
+      oldFlags = git.makeFlags or [];
+      newVal = "ETC_GITCONFIG=${cfg.config}";
+    in if isList oldFlags
+       then oldFlags ++ [ newVal ]
+       else "${oldFlags} ${newVal}";
+  });
+in {
+  options.vuizvui.user.aszlig.programs.git = {
+    enable = mkEnableOption "Git";
+
+    config = mkOption {
+      description = "System-wide default config for Git";
+
+      type = with types; let
+        options = attrsOf (either (either bool int) str);
+        subSection = addCheck (attrsOf options) (s: all isAttrs (attrValues s));
+      in attrsOf (either subSection options);
+
+      default = {};
+      example = {
+        color.ui = "auto";
+        merge.tool = "vimdiff";
+        guitool.foobar.noconsole = true;
+      };
+
+      apply = genConf;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      gitPatched
+      pkgs.gitAndTools.git-remote-hg
+      pkgs.gitAndTools.hub
+    ];
+  };
+}
diff --git a/modules/user/aszlig/programs/mpv/default.nix b/modules/user/aszlig/programs/mpv/default.nix
new file mode 100644
index 00000000..927c76b8
--- /dev/null
+++ b/modules/user/aszlig/programs/mpv/default.nix
@@ -0,0 +1,24 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.aszlig.programs.mpv;
+
+  patchedMpv = pkgs.mpv.overrideAttrs (drv: {
+    postInstall = (drv.postInstall or "") + ''
+      mkdir -p "$out/etc/mpv"
+      cat > "$out/etc/mpv/mpv.conf" <<CONFIG
+      ao=pulse
+      ytdl-format=bestvideo[height <= 1080]+bestaudio/best
+      CONFIG
+    '';
+  });
+
+in {
+  options.vuizvui.user.aszlig.programs.mpv = {
+    enable = lib.mkEnableOption "aszlig's MPV";
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ patchedMpv ];
+  };
+}
diff --git a/modules/user/aszlig/programs/taskwarrior/config.patch b/modules/user/aszlig/programs/taskwarrior/config.patch
new file mode 100644
index 00000000..4ee4c4ce
--- /dev/null
+++ b/modules/user/aszlig/programs/taskwarrior/config.patch
@@ -0,0 +1,48 @@
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 5558f6b..c8956f8 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -91,6 +91,9 @@ SET (TASK_DOCDIR  share/doc/task CACHE STRING "Installation directory for doc fi
+ SET (TASK_RCDIR "${TASK_DOCDIR}/rc" CACHE STRING "Installation directory for configuration files")
+ SET (TASK_BINDIR  bin            CACHE STRING "Installation directory for the binary")
+ 
++SET (SYSTEM_TASKRC "${CMAKE_INSTALL_PREFIX}/etc/taskrc"
++     CACHE STRING "System-wide taskrc")
++
+ message ("-- Looking for SHA1 references")
+ if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
+   set (HAVE_COMMIT true)
+diff --git a/cmake.h.in b/cmake.h.in
+index 0041e6e..f8c1a0e 100644
+--- a/cmake.h.in
++++ b/cmake.h.in
+@@ -16,6 +16,7 @@
+ 
+ /* Installation details */
+ #define TASK_RCDIR "${CMAKE_INSTALL_PREFIX}/${TASK_RCDIR}"
++#define SYSTEM_TASKRC "${SYSTEM_TASKRC}"
+ 
+ /* Localization */
+ #define PACKAGE_LANGUAGE ${PACKAGE_LANGUAGE}
+diff --git a/src/Context.cpp b/src/Context.cpp
+index 8aae74e..ffa5557 100644
+--- a/src/Context.cpp
++++ b/src/Context.cpp
+@@ -121,7 +121,8 @@ int Context::initialize (int argc, const char** argv)
+     }
+ 
+     config.clear ();
+-    config.load (rc_file);
++    config.load (SYSTEM_TASKRC);
++    config.load (rc_file, 2);
+     CLI2::applyOverrides (argc, argv);
+ 
+     ////////////////////////////////////////////////////////////////////////////
+@@ -146,7 +147,6 @@ int Context::initialize (int argc, const char** argv)
+     }
+ 
+     tdb2.set_location (data_dir);
+-    createDefaultConfig ();
+ 
+     ////////////////////////////////////////////////////////////////////////////
+     //
diff --git a/modules/user/aszlig/programs/taskwarrior/default.nix b/modules/user/aszlig/programs/taskwarrior/default.nix
new file mode 100644
index 00000000..99d428de
--- /dev/null
+++ b/modules/user/aszlig/programs/taskwarrior/default.nix
@@ -0,0 +1,36 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.aszlig.programs.taskwarrior;
+
+  taskrc = pkgs.writeText "taskrc.in" ''
+    data.location=~/.task
+    include @out@/share/doc/task/rc/dark-yellow-green.theme
+
+    color=on
+    dateformat=Y-m-d
+    dateformat.annotation=Y-m-d
+    dateformat.edit=Y-m-d H:N:S
+    dateformat.holiday=YMD
+    dateformat.info=Y-m-d H:N:S
+    dateformat.report=Y-m-d
+    weekstart=Monday
+  '';
+
+  taskwarrior = pkgs.taskwarrior.overrideDerivation (t: {
+    patches = (t.patches or []) ++ [ ./config.patch ];
+    postInstall = (t.postInstall or "") + ''
+      mkdir -p "$out/etc"
+      substituteAll "${taskrc}" "$out/etc/taskrc"
+    '';
+  });
+
+in {
+  options.vuizvui.user.aszlig.programs.taskwarrior = {
+    enable = lib.mkEnableOption "aszlig's TaskWarrior";
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = lib.singleton taskwarrior;
+  };
+}
diff --git a/modules/user/aszlig/programs/zsh/default.nix b/modules/user/aszlig/programs/zsh/default.nix
new file mode 100644
index 00000000..a97e000a
--- /dev/null
+++ b/modules/user/aszlig/programs/zsh/default.nix
@@ -0,0 +1,129 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.user.aszlig.programs.zsh;
+  inherit (cfg) machineColor;
+
+in {
+  options.vuizvui.user.aszlig.programs.zsh = {
+    enable = mkEnableOption "zsh";
+
+    machineColor = mkOption {
+      type = types.enum [
+        "black" "red" "green" "yellow" "blue" "magenta" "cyan" "white"
+      ];
+      default = "red";
+      example = "green";
+      description = ''
+        The color used for coloring the machine name in the prompt.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.shellInit = ''
+      export EDITOR="vim"
+      export EMAIL="aszlig@nix.build"
+    '';
+
+    nixpkgs.overlays = singleton (lib.const (super: {
+      zsh = overrideDerivation super.zsh (o: {
+        postConfigure = (o.postConfigure or "") + ''
+          sed -i -e '/^name=zsh\/newuser/d' config.modules
+        '';
+      });
+    }));
+
+    programs.zsh.enable = true;
+
+    programs.zsh.shellAliases.t = "task";
+    programs.zsh.shellAliases.p = "gopass";
+
+    programs.zsh.setOptions = lib.mkForce [
+      "auto_cd"
+      "auto_pushd"
+      "beep"
+      "correct"
+      "dvorak"
+      "extended_glob"
+      "extended_history"
+      "hist_fcntl_lock"
+      "hist_ignore_dups"
+      "hist_no_store"
+      "hist_reduce_blanks"
+      "interactive_comments"
+    ];
+
+    programs.zsh.interactiveShellInit = mkAfter ''
+      export HISTFILE=~/.histfile
+      export HISTSIZE=100000
+      export SAVEHIST=100000
+      export KEYTIMEOUT=1
+
+      bindkey -v
+      if [[ "$TERM" = xterm ]]; then
+        bindkey -v '\e[H' vi-beginning-of-line
+        bindkey -v '\e[F' vi-end-of-line
+
+        function set-title() {
+          echo -en "\e]2;$2\a"
+        }
+
+        function reset-title() {
+          echo -en "\e]2;''${(%):-%~}\a\a"
+        }
+
+        autoload -Uz add-zsh-hook
+        add-zsh-hook preexec set-title
+        add-zsh-hook precmd reset-title
+      else
+        bindkey -v '\e[1~' vi-beginning-of-line
+        bindkey -v '\e[4~' vi-end-of-line
+      fi
+
+      bindkey -a '/' history-incremental-pattern-search-backward
+      bindkey -a '?' history-incremental-pattern-search-forward
+      bindkey '\e[A' up-line-or-history
+      bindkey '\e[B' down-line-or-history
+
+      zstyle ':completion:*' completer _expand _complete _ignored _approximate
+      zstyle ':completion:*' expand prefix suffix
+      zstyle ':completion:*' group-name '''
+      zstyle ':completion:*' insert-unambiguous true
+      zstyle ':completion:*' list-colors '''
+      zstyle ':completion:*' list-prompt \
+        %SAt %p: Hit TAB for more, or the character to insert%s
+      zstyle ':completion:*' list-suffixes true
+      zstyle ':completion:*' matcher-list ''' \
+        'm:{[:lower:]}={[:upper:]}' \
+        'm:{[:lower:][:upper:]}={[:upper:][:lower:]}' \
+        'l:|=* r:|=*' \
+        'r:|[._-]=** r:|=**'
+      zstyle ':completion:*' max-errors 2 numeric
+      zstyle ':completion:*' menu select=long
+      zstyle ':completion:*' original true
+      zstyle ':completion:*' preserve-prefix '//[^/]##/'
+      zstyle ':completion:*' prompt \
+        'Hm, did you mistype something? There are %e errors in the completion.'
+      zstyle ':completion:*' select-prompt \
+        %SScrolling active: current selection at %p%s
+      zstyle ':completion:*' use-compctl false
+      zstyle ':completion:*' verbose true
+
+      autoload -Uz zmv
+    '';
+
+    programs.zsh.promptInit = ''
+      autoload -Uz prompt_special_chars
+
+      () {
+          local p_machine='%(!..%B%F{red}%n%b%F{blue}@)%b%F{${machineColor}}%m'
+          local p_path='%B%F{blue}[%F{cyan}%~%B%F{blue}]'
+          local p_exitcode='%F{green}%?%(!.%F{cyan}>.%b%F{green}>)%b%f '
+          PROMPT="$p_machine$p_path$p_exitcode"
+      }
+    '';
+  };
+}
diff --git a/modules/user/aszlig/services/i3/conky.nix b/modules/user/aszlig/services/i3/conky.nix
new file mode 100644
index 00000000..6c5815aa
--- /dev/null
+++ b/modules/user/aszlig/services/i3/conky.nix
@@ -0,0 +1,122 @@
+{ pkgs ? import (import ../../../../../nixpkgs-path.nix) {}
+, lib ? import "${import ../../../../../nixpkgs-path.nix}/lib"
+, timeout ? 300
+}:
+
+with lib;
+
+let
+  baseConfig = pkgs.writeText "conkyrc" ''
+    conky.config = {
+      cpu_avg_samples = 2,
+      net_avg_samples = 2,
+      no_buffers = true,
+      out_to_console = true,
+      out_to_ncurses = false,
+      out_to_stderr = false,
+      out_to_x = false,
+      extra_newline = false,
+      update_interval = 1.0,
+      uppercase = false,
+      use_spacer = 'none',
+      pad_percents = 3,
+      use_spacer = 'left',
+    };
+
+    conky.text = ''';
+  '';
+
+  optexpr = name: expr: "\${${name}_disabled:-\\\${${name} ${expr}\\}}";
+  cexpr = name: args: "${optexpr name (concatStringsSep " " args)}";
+
+  mkNetInfo = iface: let
+    upspeed = cexpr "upspeed" [ iface ];
+    downspeed = cexpr "downspeed" [ iface ];
+  in "${upspeed} ${downspeed}";
+
+  mkDiskFree = path: let
+    used = cexpr "fs_used" [ path ];
+    size = cexpr "fs_size" [ path ];
+  in "${used}/${size}";
+
+  gpuTemp = "${cexpr "hwmon" [ "0" "temp" "1" ]}C";
+
+  weather = (cexpr "weather" [
+    "http://tgftp.nws.noaa.gov/data/observations/metar/stations/"
+    "EDMA"
+    "temperature"
+  ]) + "C";
+
+  mkConky = args: let
+    time = cexpr "time" [ "%a %b %d %T %Z %Y" ];
+    text = concatStringsSep " | " (args ++ singleton time);
+    conky = pkgs.conky.override {
+      weatherMetarSupport = true;
+    };
+  in pkgs.writeScript "conky-run.sh" ''
+    #!${pkgs.stdenv.shell}
+    PATH="${pkgs.coreutils}/bin"
+
+    cpuload() {
+      for i in $(seq 1 $(nproc))
+      do
+        [ $i -eq 1 ] || echo -n ' '
+        echo -n "\''${cpu cpu$i}%"
+      done
+    }
+
+    cputemp_collect() {
+      for i in /sys/bus/platform/devices/coretemp.?/hwmon/hwmon?/temp?_input
+      do
+        [ -e "$i" ] || continue
+        echo "$i" | ${pkgs.gnused}/bin/sed -re \
+          's/^.*hwmon([0-9]+)[^0-9]*([0-9]+).*$/''${hwmon \1 temp \2}/'
+      done
+    }
+
+    cputemp() {
+      echo $(cputemp_collect)
+    }
+
+    tries=0
+    while ! raw_netinfo="$(${
+      "${pkgs.iproute}/sbin/ip route get 8.8.8.8 2> /dev/null"
+    })"; do
+      if [ $tries -ge ${toString timeout} ]; then
+        upspeed_disabled=N/A
+        downspeed_disabled=N/A
+        break
+      fi
+      echo "Waiting for primary network interface to become available..."
+      tries=$(($tries + 1))
+      sleep 1
+    done
+
+    primary_netdev="$(echo "$raw_netinfo" | \
+      ${pkgs.gnused}/bin/sed -nre 's/^.*dev *([^ ]+).*$/\1/p')"
+
+    # FIXME: Log stderr to the journal!
+    ${conky}/bin/conky -c "${baseConfig}" -t "${text}" 2> /dev/null
+  '';
+
+in {
+  left = mkConky [
+    "CPU: $(cpuload) - ${cexpr "cpu" [ "cpu0" ]}%"
+    "MEM: \\$mem/\\$memmax - \\$memperc%"
+    "SWAP: \\$swap/\\$swapmax \\$swapperc%"
+  ];
+
+  right = mkConky [
+    "NET: ${mkNetInfo "$primary_netdev"}"
+    "DF: ${mkDiskFree "/"}"
+    "LAVG: \\$loadavg"
+    "TEMP - CPU: $(cputemp) - GPU: ${gpuTemp} - OUTSIDE: ${weather}"
+  ];
+
+  single = mkConky [
+    "CPU: $(cpuload) - ${cexpr "cpu" [ "cpu0" ]}%"
+    "MEM: \\$mem/\\$memmax - \\$memperc%"
+    "NET: ${mkNetInfo "$primary_netdev"}"
+    "TEMP - CPU: $(cputemp) - OUTSIDE: ${weather}"
+  ];
+}
diff --git a/modules/user/aszlig/services/i3/default.nix b/modules/user/aszlig/services/i3/default.nix
new file mode 100644
index 00000000..733541ee
--- /dev/null
+++ b/modules/user/aszlig/services/i3/default.nix
@@ -0,0 +1,132 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.user.aszlig.services.i3;
+  inherit (config.services.xserver) xrandrHeads;
+
+  # The symbols if you press shift and a number key.
+  wsNumberSymbols = [
+    "exclam" "at" "numbersign" "dollar" "percent"
+    "asciicircum" "ampersand" "asterisk" "parenleft" "parenright"
+  ];
+
+  wsCount = length wsNumberSymbols;
+
+  headCount = length xrandrHeads;
+  wsPerHead = wsCount / headCount;
+  excessWs = wsCount - (headCount * wsPerHead);
+  headModifier = if cfg.reverseHeads then reverseList else id;
+  getHeadAt = x: (elemAt (headModifier xrandrHeads) x).output;
+
+  mkSwitchTo = number: "$mod+${if number == 10 then "0" else toString number}";
+
+  mkDefaultWorkspace = number: numberSymbol: {
+    name = toString number;
+    value = {
+      label = mkDefault null;
+      labelPrefix = mkDefault "${toString number}: ";
+      keys.switchTo = mkDefault (mkSwitchTo number);
+      keys.moveTo = mkDefault "$mod+Shift+${numberSymbol}";
+      head = if headCount == 0 then mkDefault null
+             else mkDefault (getHeadAt ((number - (excessWs + 1)) / wsPerHead));
+    };
+  };
+
+  wsCfgList = mapAttrsToList (_: getAttr "config") cfg.workspaces;
+  wsConfig = concatStrings wsCfgList;
+  defaultWorkspaces = listToAttrs (imap mkDefaultWorkspace wsNumberSymbols);
+
+  conky = import ./conky.nix {
+    inherit pkgs lib;
+    timeout = cfg.networkTimeout;
+  };
+
+  mkBar = output: statusCmd: singleton ''
+    bar {
+      ${optionalString (output != null) "output ${output}"}
+      ${optionalString (statusCmd != null) "status_command ${statusCmd}"}
+      colors {
+        focused_workspace  #5c5cff #e5e5e5
+        active_workspace   #ffffff #0000ee
+        inactive_workspace #00cdcd #0000ee
+        urgent_workspace   #ffff00 #cd0000
+      }
+    }
+  '';
+
+  barConfig = let
+    barHeads = map (h: h.output) (headModifier xrandrHeads);
+    bars = if headCount == 0 then mkBar null conky.single
+      else if headCount == 1 then mkBar (head barHeads) conky.single
+      else let inner = take (length barHeads - 2) (tail barHeads);
+           in mkBar (head barHeads) conky.left
+           ++ map (flip mkBar null) inner
+           ++ mkBar (last barHeads) conky.right;
+  in concatStrings (headModifier bars);
+
+in
+{
+  options.vuizvui.user.aszlig.services.i3 = {
+    enable = mkEnableOption "i3";
+
+    workspaces = mkOption {
+      type = types.attrsOf (types.submodule (import ./workspace.nix));
+      description = ''
+        Workspace to monitor assignment.
+
+        Workspaces are by default assigned starting from the leftmost monitor
+        being workspace 1 and the rightmost monitor being workspace 10. The
+        workspaces are divided by the number of available heads, so if you have
+        a dual head system, you'll end up having workspace 1 to 5 on the left
+        monitor and 6 to 10 on the right.
+      '';
+    };
+
+    reverseHeads = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Reverse the order of the heads, so if enabled and you have two heads,
+        you'll end up having workspaces 1 to 5 on the right head and 6 to 10 on
+        the left head.
+      '';
+    };
+
+    networkTimeout = mkOption {
+      type = types.int;
+      default = 300;
+      description = ''
+        Maximum number of seconds to wait for network device detection.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    vuizvui.user.aszlig.services.i3.workspaces = defaultWorkspaces;
+
+    services.xserver.windowManager = {
+      i3.enable = true;
+      i3.configFile = pkgs.substituteAll {
+        name = "i3.conf";
+        src = ./i3.conf;
+
+        inherit (pkgs) dmenu xterm;
+        inherit (pkgs.vuizvui.aszlig) pvolctrl;
+        inherit (pkgs.xorg) xsetroot;
+        inherit wsConfig barConfig;
+
+        lockall = pkgs.writeScript "lockvt.sh" ''
+          #!${pkgs.stdenv.shell}
+          "${pkgs.socat}/bin/socat" - UNIX-CONNECT:/run/console-lock.sock \
+            < /dev/null
+        '';
+
+        postInstall = ''
+          ${pkgs.i3}/bin/i3 -c "$target" -C
+        '';
+      };
+    };
+  };
+}
diff --git a/modules/user/aszlig/services/i3/i3.conf b/modules/user/aszlig/services/i3/i3.conf
new file mode 100644
index 00000000..cd14c425
--- /dev/null
+++ b/modules/user/aszlig/services/i3/i3.conf
@@ -0,0 +1,131 @@
+# default modifier key
+set $mod Mod4
+
+# we want to have a VT-style font :-)
+font -dosemu-vga-medium-r-normal--17-160-75-75-c-80-ibm-cp866
+
+# Use Mouse+$mod to drag floating windows to their wanted position
+floating_modifier $mod
+
+# reasonable defaults!
+default_orientation horizontal
+workspace_layout tabbed
+popup_during_fullscreen ignore
+
+# start a terminal
+bindsym $mod+Shift+Return exec --no-startup-id @xterm@/bin/xterm
+
+# kill focused window
+bindsym $mod+Shift+C kill
+
+# start dmenu (a program launcher)
+bindsym $mod+p exec --no-startup-id @dmenu@/bin/dmenu_run
+
+# start lock screen
+bindsym $mod+Shift+Escape exec --no-startup-id @lockall@
+
+# set background
+exec @xsetroot@/bin/xsetroot -solid black
+
+# audio controls
+bindsym XF86AudioLowerVolume exec @pvolctrl@/bin/pvolctrl -10
+bindsym XF86AudioRaiseVolume exec @pvolctrl@/bin/pvolctrl 10
+bindsym XF86AudioMute exec @pvolctrl@/bin/pvolctrl 0
+
+# change/move focus
+bindsym $mod+Shift+Left move left
+bindsym $mod+Shift+H move left
+bindsym $mod+Shift+Down move down
+bindsym $mod+Shift+T move down
+bindsym $mod+Shift+Up move up
+bindsym $mod+Shift+N move up
+bindsym $mod+Shift+Right move right
+bindsym $mod+Shift+S move right
+
+bindsym $mod+Left focus left
+bindsym $mod+h focus left
+bindsym $mod+Down focus down
+bindsym $mod+t focus down
+bindsym $mod+Up focus up
+bindsym $mod+n focus up
+bindsym $mod+Right focus right
+bindsym $mod+s focus right
+
+# split in horizontal orientation
+bindsym $mod+i split h
+
+# split in vertical orientation
+bindsym $mod+d split v
+
+# enter fullscreen mode for the focused container
+bindsym $mod+f fullscreen
+
+# change container layout (stacked, tabbed, default)
+bindsym $mod+apostrophe layout stacking
+bindsym $mod+comma layout tabbed
+bindsym $mod+period layout default
+
+# 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+semicolon focus child
+
+# reload the configuration file
+bindsym $mod+Shift+L 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+Q exit
+
+# resize window (you can also use the mouse for that)
+mode "resize" {
+    # These bindings trigger as soon as you enter the resize mode
+
+    # They resize the border in the direction you pressed, e.g.
+    # when pressing left, the window is resized so that it has
+    # more space on its left
+
+    bindsym Left resize shrink left 10 px or 10 ppt
+    bindsym h resize shrink left 10 px or 10 ppt
+    bindsym Down resize shrink down 10 px or 10 ppt
+    bindsym t resize shrink down 10 px or 10 ppt
+    bindsym Up resize shrink up 10 px or 10 ppt
+    bindsym n resize shrink up 10 px or 10 ppt
+    bindsym Right resize shrink right 10 px or 10 ppt
+    bindsym s resize shrink right 10 px or 10 ppt
+
+    bindsym Shift+Left resize grow left 10 px or 10 ppt
+    bindsym Shift+H resize grow left 10 px or 10 ppt
+    bindsym Shift+Down resize grow down 10 px or 10 ppt
+    bindsym Shift+T resize grow down 10 px or 10 ppt
+    bindsym Shift+Up resize grow up 10 px or 10 ppt
+    bindsym Shift+N resize grow up 10 px or 10 ppt
+    bindsym Shift+Right resize grow right 10 px or 10 ppt
+    bindsym Shift+S resize grow right 10 px or 10 ppt
+
+    # back to normal: Enter or Escape
+    bindsym Return mode "default"
+    bindsym Escape mode "default"
+}
+
+bindsym $mod+r mode "resize"
+
+# workspace configuration
+@wsConfig@
+
+# ratmenu should be as unintrusive as possible
+for_window [class="^ratmenu$"] floating enable
+for_window [class="^ratmenu$"] border none
+
+# various app cruft
+for_window [class="^Dia$"] floating enable
+
+# bar configuration
+@barConfig@
diff --git a/modules/user/aszlig/services/i3/workspace.nix b/modules/user/aszlig/services/i3/workspace.nix
new file mode 100644
index 00000000..403ba57d
--- /dev/null
+++ b/modules/user/aszlig/services/i3/workspace.nix
@@ -0,0 +1,107 @@
+{ name, lib, config, ... }:
+
+with lib;
+
+let
+  finalLabel =
+    if config.label == null then name
+    else config.labelPrefix + config.label;
+
+  mkDoc = anchor: "http://i3wm.org/docs/userguide.html#${anchor}";
+in
+{
+  options = {
+    labelPrefix = mkOption {
+      type = types.str;
+      default = "";
+      example = "666: ";
+      description = ''
+        The value that will be put in front of the <option>label</option>.
+        So if you have a label called <replaceable>bar</replaceable> and a
+        <option>labelPrefix</option> called <replaceable>foo</replaceable> the
+        label for the workspace will be <replaceable>foobar</replaceable>.
+      '';
+    };
+
+    label = mkOption {
+      type = types.nullOr types.str;
+      default = name;
+      description = ''
+        The label of this workspace, which is its name by default. If the value
+        is <replaceable>null</replaceable>, the resulting label of the workspace
+        is just its name and no <option>labelPrefix</option> is applied.
+      '';
+    };
+
+    assign = mkOption {
+      type = types.listOf types.attrs;
+      default = [];
+      example = [
+        { class = "^Chromium(?:-browser)?\$"; }
+        { instance = "^gajim\$"; }
+      ];
+      description = let
+        anchor = "_automatically_putting_clients_on_specific_workspaces";
+      in ''
+        Assign windows to this specific workspace using the attribute names
+        described by <link xlink:href="${mkDoc anchor}"/>.
+      '';
+    };
+
+    head = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The XRandR head this workspace will be assigned to.
+      '';
+    };
+
+    keys = let
+      commonDesc = ''
+        The <replaceable>$mod</replaceable> placeholder represents the default
+        modifier key. Details about the syntax of key combinations can be found
+        at <link xlink:href="${mkDoc "keybindings"}"/>.
+      '';
+    in {
+      switchTo = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "$mod+1";
+        description = ''
+          Key combination to switch to this workspace.
+        '' + commonDesc;
+      };
+
+      moveTo = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "$mod+Shift+exclam";
+        description = ''
+          Key combination to move a container to this workspace.
+        '' + commonDesc;
+      };
+    };
+
+    config = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Raw configuration options for this workspace.
+      '';
+    };
+  };
+
+  config.config = let
+    mkAssign = mapAttrsToList (criteria: value: "${criteria}=\"${value}\"");
+    mkSym = sym: rest: optionalString (sym != null) "bindsym ${sym} ${rest}";
+  in ''
+    ${optionalString (config.head != null) ''
+    workspace "${finalLabel}" output ${config.head}
+    ''}
+    ${mkSym config.keys.switchTo "workspace \"${finalLabel}\""}
+    ${mkSym config.keys.moveTo "move workspace \"${finalLabel}\""}
+    ${concatMapStrings (assign: ''
+    assign [${concatStringsSep " " (mkAssign assign)}] ${finalLabel}
+    '') config.assign}
+  '';
+}
diff --git a/modules/user/aszlig/services/vlock/default.nix b/modules/user/aszlig/services/vlock/default.nix
new file mode 100644
index 00000000..c76e176f
--- /dev/null
+++ b/modules/user/aszlig/services/vlock/default.nix
@@ -0,0 +1,67 @@
+{ pkgs, config, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.aszlig.services.vlock;
+
+  messageFile = pkgs.runCommandLocal "message.cat" {} ''
+    echo -en '\e[H\e[2J\e[?25l' > "$out"
+    "${pkgs.vuizvui.aszlig.aacolorize}/bin/aacolorize" \
+      "${./message.cat}" "${./message.colmap}" \
+      >> "$out"
+  '';
+
+  esc = "\\\\033";
+  unlockCSI = "${esc}[16;39H${esc}[?25h${esc}[K";
+
+  vlock = lib.overrideDerivation pkgs.vlock (o: {
+    postPatch = (o.postPatch or "") + ''
+      echo -n '"' > src/message.h
+      sed -e ':nl;N;$!bnl;s/[\\"]/\\&/g;s/\n/\\n/g' "${messageFile}" \
+        >> src/message.h
+      sed -i -e '$s/$/"/' src/message.h
+      sed -i -e 's!getenv("VLOCK_MESSAGE")!\n#include "message.h"\n!' \
+        src/vlock-main.c
+      sed -i -re 's/(fprintf[^"]*")(.*user)/\1${unlockCSI}\2/' \
+        src/auth-pam.c
+    '';
+  });
+in {
+  options.vuizvui.user.aszlig.services.vlock = {
+    enable = lib.mkEnableOption "console lock";
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      example = "horst";
+      internal = true;
+      description = ''
+        The user under which the locked session will run. This is a workaround
+        and thus this option is solely internal.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.sockets.vlock = {
+      description = "Console Lock Socket";
+      wantedBy = [ "sockets.target" ];
+      socketConfig.ListenStream = "/run/console-lock.sock";
+      socketConfig.Accept = true;
+    };
+
+    systemd.services."vlock@" = {
+      description = "Lock All Consoles";
+      serviceConfig.Type = "oneshot";
+
+      #environment.USER = "%i"; XXX
+      environment.USER = cfg.user;
+
+      script = ''
+        retval=0
+        oldvt="$("${pkgs.kbd}/bin/fgconsole")"
+        "${vlock}/bin/vlock" -asn || retval=$?
+        if [ $retval -ne 0 ]; then "${pkgs.kbd}/bin/chvt" "$oldvt"; fi
+        exit $retval
+      '';
+    };
+  };
+}
diff --git a/modules/user/aszlig/services/vlock/message.cat b/modules/user/aszlig/services/vlock/message.cat
new file mode 100644
index 00000000..f079e829
--- /dev/null
+++ b/modules/user/aszlig/services/vlock/message.cat
@@ -0,0 +1,18 @@
+
+                .
+                |
+          -_    |     .           .-.  .-. ..      ,.--., ,===.
+            `-_ |     |           '||\.||' `' ,  , ||  || ;___
+    -_         >:_    |    _-      ||`\||  || `\/' ||  ||     ;
+      `-_   _-'   `-_ | _-'       .'   `|  ;' /'`\ ``=='' ,==='
+         >:'         `:'
+      _-' |           |    _-   ..              ..             ..
+    -'    |           | _-'     ||              ||             ||
+         .|.         _:<        ||  ,---. .---. ||,-. .--.  .--||
+      _-' | `-_   _-'   `-_     ||  ||"|| ||''' |.,'' |"/'  |,";|
+    -'    |    `:<         `-   ||_ ||_|| ||__  |,\\. ||__  ||_,|
+          |     | `-_           `--'`---' `---' '' `' `---' `---'
+          '     |    `-
+                |                     press ENTER to unlock
+                `
+
diff --git a/modules/user/aszlig/services/vlock/message.colmap b/modules/user/aszlig/services/vlock/message.colmap
new file mode 100644
index 00000000..d7e42fb6
--- /dev/null
+++ b/modules/user/aszlig/services/vlock/message.colmap
@@ -0,0 +1,18 @@
+
+                c
+                c
+          cc    c     b           WWW  WWW WW      BccccB cBBBc
+            ccc c     b           WWWWWWWW WW W  W Bc  cB cccc
+    bb         ccc    b    bb      WWWWWW  WW WWWW Bc  cB     c
+      bbb   bbb   ccc b bbb       WW   WW  WW WWWW BcBBcB cBBBc
+         bbb         cbb
+      bbb c           b    cc   rr              rr             rr
+    bb    c           b ccc     rr              rr             rr
+         ccb         ccc        rr  rrrrr rrrrr rrrrr rrrr  rrrrr
+      ccc c bbb   ccc   ccc     rr  rrRrr rrRRR rrrrr rRrr  rrRrr
+    cc    c    bbb         cc   rrr rrrrr rrrr  rrrrr rrrr  rrrrr
+          c     b bbb           rrrrrrrrr rrrrr rr rr rrrrr rrrrr
+          c     b    bb
+                b                     ppppp PPPPP pp pppppp
+                b
+
diff --git a/modules/user/devhell/profiles/base.nix b/modules/user/devhell/profiles/base.nix
new file mode 100644
index 00000000..5e0ccc76
--- /dev/null
+++ b/modules/user/devhell/profiles/base.nix
@@ -0,0 +1,128 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.devhell.profiles.base;
+
+in {
+  options.vuizvui.user.devhell.profiles.base = {
+    enable = lib.mkEnableOption "Base profile for devhell";
+  };
+
+  config = lib.mkIf cfg.enable {
+    boot = {
+      kernelPackages = pkgs.linuxPackages_5_4;
+      cleanTmpDir = true;
+    };
+
+    nix = {
+      buildCores = 0;
+      useSandbox = true;
+    };
+
+    time = {
+      timeZone = "Europe/London";
+    };
+
+    system = {
+      fsPackages = with pkgs; [
+        sshfsFuse
+        fuse
+        cryptsetup
+      ];
+    };
+
+    hardware = {
+      enableAllFirmware = true;
+      nitrokey.enable = true;
+      u2f.enable = true;
+      opengl = {
+        s3tcSupport = true;
+        driSupport32Bit = true;
+      };
+      pulseaudio = {
+        enable = true;
+        systemWide = false;
+        extraConfig = "load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1";
+      };
+    };
+
+    networking = {
+      firewall.enable = false;
+      useDHCP = false;
+    };
+
+    users.users.dev = {
+      isNormalUser = true;
+      extraGroups = [ "nitrokey" "plugdev" "docker" "vboxusers" "wheel" "mpd" "libvirtd" "wireshark" "video" "audio" ];
+      uid = 1000;
+      shell = "${pkgs.zsh}/bin/zsh";
+    };
+
+    programs = {
+      gnupg = {
+        agent.enable = true;
+        agent.pinentryFlavor = "gnome3";
+      };
+      ssh = {
+        startAgent = false;
+      };
+      zsh = {
+        enable = true;
+        enableCompletion = true;
+      };
+      bash = {
+        enableCompletion = true;
+        promptInit = ''
+          # Provide a nice prompt.
+          PROMPT_COLOR="1;31m"
+          let $UID && PROMPT_COLOR="1;32m"
+          PS1="\n\[\033[$PROMPT_COLOR\][\u@\h:\w]\\$\[\033[0m\] "
+          if test "$TERM" = "xterm"; then
+            PS1="\[\033]2;\h:\u:\w\007\]$PS1"
+          fi
+          eval `dircolors ~/.dir_colors`
+        '';
+      };
+    };
+
+    environment = {
+      shells = [ "/run/current-system/sw/bin/zsh" ];
+    };
+
+    fonts = {
+      fontconfig = {
+        enable = true;
+      };
+      enableGhostscriptFonts = true;
+      fonts = with pkgs; [
+        cascadia-code
+        clearlyU
+        cm_unicode
+        corefonts
+        dejavu_fonts
+        dosemu_fonts
+        fira-code
+        font-awesome
+        freefont_ttf
+        google-fonts
+        hack-font
+        inconsolata
+        junicode
+        powerline-fonts
+        proggyfonts
+        siji
+        source-code-pro
+        source-sans-pro
+        source-serif-pro
+        terminus_font
+        tewi-font
+        tt2020
+        ttf_bitstream_vera
+        ubuntu_font_family
+        unifont
+        vistafonts
+        wqy_microhei
+      ] ++ lib.filter lib.isDerivation (lib.attrValues lohit-fonts);
+    };
+  };
+}
diff --git a/modules/user/devhell/profiles/packages.nix b/modules/user/devhell/profiles/packages.nix
new file mode 100644
index 00000000..c7beed9a
--- /dev/null
+++ b/modules/user/devhell/profiles/packages.nix
@@ -0,0 +1,283 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.devhell.profiles.packages;
+
+in {
+  options.vuizvui.user.devhell.profiles.packages = {
+    enable = lib.mkEnableOption "Packages profile for devhell";
+  };
+
+  config = lib.mkIf cfg.enable {
+    nixpkgs.overlays = lib.singleton (lib.const (super: {
+      ncmpcpp = super.ncmpcpp.override {
+        visualizerSupport = true;
+        clockSupport = true;
+      };
+
+      polybar = super.polybar.override {
+        nlSupport = true;
+        pulseSupport = true;
+        i3GapsSupport = true;
+        mpdSupport = true;
+      };
+
+      sox = super.sox.override {
+        enableLame = true;
+      };
+    }));
+
+    nixpkgs.config = {
+      pulseaudio = true;
+
+      allowUnfree = true;
+
+      systemd = {
+        enableKDbus = true;
+      };
+
+      conky = {
+        weatherMetarSupport = true;
+        mpdSupport = true;
+        wirelessSupport = true;
+        x11Support = false;
+      };
+
+      firefox = {
+        enableGTK3 = true;
+        enableOfficalBranding = true;
+      };
+
+      mpv = {
+        youtubeSupport = true;
+      };
+    };
+
+    environment.systemPackages = with pkgs; [
+      #electricsheep
+      #ntopng
+      #texlive.combined.scheme-small
+      abook
+      accountsservice
+      ag
+      alacritty
+      antiword
+      apg
+      aria2
+      ascii
+      aspell
+      aspellDicts.de
+      aspellDicts.en
+      axel
+      bc
+      bcal
+      beets
+      binutils
+      bmon
+      brave
+      broot
+      cataclysm-dda
+      ccrypt
+      chromaprint
+      cifs_utils
+      cipherscan
+      clac
+      cmatrix
+      colordiff
+      cryptsetup
+      cuetools
+      darkstat
+      dcfldd
+      ddrescue
+      dhcping
+      di
+      dmidecode
+      dos2unix
+      duff
+      e2fsprogs
+      enhanced-ctorrent
+      ethtool
+      fbida
+      fd
+      fdupes
+      feh
+      ffmpeg-full
+      figlet
+      file
+      firefox
+      flac
+      focuswriter
+      fortune
+      freerdpUnstable
+      fuse_exfat
+      fzf
+      gcc
+      gdb
+      ghostscript
+      ghostwriter
+      git
+      gitinspector
+      glow
+      gnufdisk
+      gnumake
+      gnupg
+      gopass
+      gotop
+      gpgme
+      gpodder
+      gptfdisk
+      graphviz
+      gstreamer
+      hdparm
+      hexedit
+      hplipWithPlugin
+      htop
+      httpie
+      i3lock-fancy
+      iftop
+      imagemagick
+      iotop
+      ipcalc
+      iprange
+      iptraf-ng
+      ipv6calc
+      jfsutils
+      john
+      jwhois
+      keepassxc
+      keybase
+      ldns
+      lftp
+      libarchive
+      libreoffice
+      lm_sensors
+      lsof
+      lxc
+      lynx
+      macchanger
+      manpages
+      mediainfo
+      mkvtoolnix
+      mmv
+      monkeysAudio
+      mosh
+      mp3gain
+      mpc_cli
+      mpv
+      msmtp
+      mtr
+      ncdu
+      ncmpcpp
+      neofetch
+      neomutt
+      neovim
+      nethack
+      nethogs
+      netrw
+      netsniff-ng
+      nitrogen
+      nixops
+      nload
+      nmap
+      ntfs3g
+      ntfsprogs
+      openssl
+      p7zip
+      pamixer
+      pandoc
+      paperkey
+      pbzip2
+      pciutils
+      pigz
+      pixz
+      polybar
+      posix_man_pages
+      powertop
+      profanity
+      pulsemixer
+      pv
+      pxz
+      qemu
+      qrencode
+      recode
+      reptyr
+      ripgrep
+      rofi
+      rstudio
+      rsync
+      safecopy
+      screen
+      scrot
+      shntool
+      smartmontools
+      sox
+      speedtest-cli
+      spek
+      ssdeep
+      starship
+      stow
+      strace
+      sxiv
+      taskell
+      tasksh
+      taskwarrior
+      taizen
+      telnet
+      termdown
+      termite
+      termshark
+      testdisk
+      tig
+      tldr
+      tmux
+      toilet
+      transcode
+      tree
+      tty-clock
+      units
+      unrar
+      unzip
+      urlview
+      usbutils
+      valgrind
+      vanilla-dmz
+      vim_configurable
+      virt-viewer
+      (virtinst.override {
+        python2Packages = python2Packages.override {
+          overrides = lib.const (super: {
+            routes = super.routes.overridePythonAttrs (lib.const {
+              doCheck = false;
+            });
+          });
+        };
+      })
+      virtmanager
+      vit
+      vlc
+      vlock
+      vorbisTools
+      vorbisgain
+      vscodium
+      w3m
+      wavpack
+      weechat
+      wget
+      which
+      wipe
+      wireguard
+      wireshark
+      wordgrinder
+      xfsprogs
+      xlibs.xev
+      xscreensaver
+      youtube-dl
+      zathura
+      zbar
+      zgrviewer
+      zip
+      zotero
+      zsync
+    ];
+  };
+}
diff --git a/modules/user/devhell/profiles/services.nix b/modules/user/devhell/profiles/services.nix
new file mode 100644
index 00000000..321d052f
--- /dev/null
+++ b/modules/user/devhell/profiles/services.nix
@@ -0,0 +1,102 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.vuizvui.user.devhell.profiles.services;
+
+in {
+  options.vuizvui.user.devhell.profiles.services = {
+    enable = lib.mkEnableOption "Services profile for devhell";
+  };
+
+  config = lib.mkIf cfg.enable {
+    virtualisation = {
+      virtualbox = {
+        host = {
+          enable = true;
+          enableHardening = false;
+        };
+      };
+      libvirtd = {
+        enable = true;
+        qemuPackage = pkgs.qemu_kvm;
+      };
+    };
+
+    location.provider = "geoclue2";
+
+    services = {
+      keybase.enable = true;
+      pcscd.enable = true;
+      gpm.enable = true;
+      openssh.enable = true;
+      udisks2.enable = true;
+      geoip-updater.enable = true;
+      geoclue2.enable = true;
+      redshift.enable = true;
+
+      compton = {
+        enable = true;
+        vSync = true;
+        backend = "glx";
+        settings = { inactive-dim = 0.2; };
+      };
+    };
+
+
+    services.xserver = {
+      displayManager.defaultSession = "none+i3";
+      displayManager.lightdm = {
+        enable = true;
+        greeters.mini = {
+          enable = true;
+          user = "dev";
+          extraConfig = ''
+            [greeter]
+            show-password-label = true
+            password-label-text = â¯
+            show-input-cursor = false
+            [greeter-theme]
+            text-color = "#4C566A"
+            window-color = "#3B4252"
+            border-width = 0px
+            layout-space = 5
+            password-background-color = "#3B4252"
+          '';
+        };
+      };
+    };
+
+    services.xserver.windowManager.i3 = {
+      enable = true;
+      package = pkgs.i3-gaps;
+    };
+
+    services.journald.extraConfig = ''
+      SystemMaxUse = 50M
+    '';
+
+    services.mpd = {
+      enable = true;
+      extraConfig = ''
+        input {
+          plugin "curl"
+        }
+
+        audio_output {
+          type "fifo"
+          name "FIFO Output"
+          path "/tmp/mpd.fifo"
+          format "44100:16:2"
+        }
+
+        audio_output {
+          type "pulse"
+          name "Pulse Output"
+          server "127.0.0.1"
+        }
+
+        replaygain "album"
+      '';
+    };
+  };
+}
diff --git a/modules/user/openlab/base.nix b/modules/user/openlab/base.nix
new file mode 100644
index 00000000..accf0271
--- /dev/null
+++ b/modules/user/openlab/base.nix
@@ -0,0 +1,83 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.user.openlab.base;
+
+in
+{
+
+  options.vuizvui.user.openlab.base.enable =
+    mkEnableOption "the base OpenLab configuration";
+
+
+  config = mkIf cfg.enable {
+    boot.loader.grub.device = mkDefault "/dev/sda";
+    boot.loader.timeout = 2;
+
+    # tmp (and nix builds) should be fast by default
+    # might make sense to disable that on machines with little RAM
+    boot.tmpOnTmpfs = mkDefault true;
+
+    fileSystems."/" = mkDefault {
+      device = "/dev/disk/by-label/labtop";
+      fsType = "ext4";
+    };
+
+    i18n = {
+      defaultLocale = "de_DE.UTF-8";
+    };
+
+    console = {
+      font = "lat9w-16";
+      keyMap = "us";
+    };
+
+    time.timeZone = "Europe/Berlin";
+
+    hardware.enableRedistributableFirmware = true;
+
+    # TODO: filesystems 
+
+    environment.systemPackages = with pkgs; let
+      base = [
+        ack ag
+        fish
+        git
+        manpages
+        netcat-openbsd
+        python3
+        tmux
+        screen
+        vim
+        wget
+      ];
+      in base;
+
+    # manual generation takes too long on slow machines
+    programs.man.enable = mkDefault false;
+
+    services.openssh.enable = true;
+
+    networking.firewall.enable = false;
+
+    users.mutableUsers = false;
+    users.users.openlab = {
+      uid = 1000;
+      isNormalUser = true;
+      password = "openlab";
+      extraGroups = [ "wheel" ];
+      openssh.authorizedKeys.keys = lib.singleton (lib.concatStrings [
+
+        "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJhthfk38lzDvoI7lPqRneI0yBpZEhLD"
+        "GRBpcXzpPSu+V0YlgrDix5fHhBl+EKfw4aeQNvQNuAky3pDtX+BDK1b7idbz9ZMCExy2a1"
+        "kBKDVJz/onLSQxiiZMuHlAljVj9iU4uoTOxX3vB85Ok9aZtMP1rByRIWR9e81/km4HdfZT"
+        "CjFVRLWfvo0s29H7l0fnbG9bb2E6kydlvjnXJnZFXX+KUM16X11lK53ilPdPJdm87VtxeS"
+        "KZ7GOiBz6q7FHzEd2Zc3CnzgupQiXGSblXrlN22IY3IWfm5S/8RTeQbMLVoH0TncgCeenX"
+        "H7FU/sXD79ypqQV/WaVVDYMOirsnh/ philip@nyx"
+      ]);
+    };
+  };
+
+}
diff --git a/modules/user/openlab/labtops.nix b/modules/user/openlab/labtops.nix
new file mode 100644
index 00000000..14aaff05
--- /dev/null
+++ b/modules/user/openlab/labtops.nix
@@ -0,0 +1,103 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.user.openlab.labtops;
+
+in
+{
+
+  options.vuizvui.user.openlab.labtops = {
+    enable = mkEnableOption "basic shared functionality of labtops";
+  };
+
+
+  config = mkIf cfg.enable {
+
+    vuizvui.user.openlab.base.enable = true;
+
+    hardware.pulseaudio = {
+      enable = true;
+      zeroconf.discovery.enable = true;
+    };
+
+    networking.wireless = {
+      enable = true;
+      networks."Labor 2.0".psk = "nerdhoehle2";
+    };
+
+    # TODO: a way to modularly specify usage patterns (e.g. 3d-printing, arduino &c.)
+    environment.systemPackages = with pkgs; let
+      baseGUI = [
+        filezilla
+        chromium
+        gnome3.gedit
+        gmpc
+        libreoffice
+        vlc
+      ];
+      image = [
+        gimp
+        inkscape
+      ];
+      media = [
+        mpv
+        vlc
+        pavucontrol
+      ];
+      three-d = [
+        # TODO doesn’t build on i686
+        # TODO add a “packageset†mechanism
+        blender
+        # TODO build fail
+        # antimony
+      ];
+      three-d-printing = [
+        freecad
+        openscad
+        printrun
+        slic3r
+      ];
+      arduinoPkgs = [
+        ino
+        arduino
+      ];
+      tools = [
+        unzip
+      ];
+      in baseGUI ++ image ++ media
+      ++ three-d ++ three-d-printing ++ arduinoPkgs
+      ++ tools;
+
+    services.xserver = {
+      enable = true;
+      layout = "us";
+      xkbOptions = "eurosign:e";
+
+      displayManager = {
+        lightdm.enable = true;
+        lightdm.autoLogin.enable = true;
+        lightdm.autoLogin.user = "openlab";
+        sessionCommands = with pkgs; ''
+          ${xorg.xset}/bin/xset r rate 250 35
+        '';
+      };
+      desktopManager.xfce.enable = true;
+      synaptics = {
+        enable = true;
+        minSpeed = "0.5";
+        accelFactor = "0.01";
+      };
+    };
+
+    users.users.openlab.extraGroups = [ "dialout" ];
+
+    # fix for emacs
+    programs.bash.promptInit = "PS=\"# \"";
+
+    programs.man.enable = true;
+
+  };
+
+}
diff --git a/modules/user/openlab/speedtest.nix b/modules/user/openlab/speedtest.nix
new file mode 100644
index 00000000..6b6d72e3
--- /dev/null
+++ b/modules/user/openlab/speedtest.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  bin = drv: name: "${lib.getBin drv}/bin/${name}";
+  cfg = config.vuizvui.user.openlab.speedtest;
+
+  py = pkgs.runCommandLocal "speedtest.py" {} ''
+    cat ${./speedtest.py} \
+      | sed -e 's|^PING_BIN =.*$|PING_BIN = "${config.security.wrapperDir}/ping"|' \
+      > $out
+  '';
+
+  speedtest = pkgs.writeScript "speedtest" ''
+    #!${bin pkgs.bash "bash"}
+    mkdir -p "$(dirname "${cfg.outputPath}")"
+    ${bin pkgs.python3 "python3"} ${py} >> "${cfg.outputPath}"
+  '';
+
+in {
+  options.vuizvui.user.openlab.speedtest = {
+    enable = mkEnableOption "openlab speedtest";
+    outputPath = mkOption {
+      description = "File to which the results are appended.";
+      type = types.path;
+      default = "/dev/null";
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    systemd.services.speedtest = {
+       description = "openlab network speedtest";
+       path = with pkgs; [ curl bind.host ];
+       environment = { "LC_ALL" = "C"; };
+       wantedBy = [ "default.target" ];
+       after = [ "network.target" ];
+       script = "${speedtest}";
+       startAt = [ "*-*-* *:00/15:00" ];
+     };
+
+     assertions = [ {
+       assertion = cfg.outputPath != "/dev/null";
+       message = "You should set `vuizvui.user.openlab.speedtest.outputPath`.";
+     } ];
+  };
+}
diff --git a/modules/user/openlab/speedtest.py b/modules/user/openlab/speedtest.py
new file mode 100755
index 00000000..384adb00
--- /dev/null
+++ b/modules/user/openlab/speedtest.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i python3 -p curl python3
+
+import sys
+import subprocess as sub
+import datetime
+
+IP = "163.172.44.192"
+DOMAIN = "haku.profpatsch.de"
+PROTOCOL = "https"
+FILE = "/pub/well-known/speedtest-5M.rng"
+SIZE = 5242880
+
+HOST_BIN = "host"
+PING_BIN = "ping"
+CURL_TIMEOUT_SEC = 30
+
+v4 = 0 == sub.run([HOST_BIN, "-4", "-W1", DOMAIN], stdout=sub.DEVNULL).returncode
+v6 = 0 == sub.run([HOST_BIN, "-6", "-W1", DOMAIN], stdout=sub.DEVNULL).returncode
+dns = v4 or v6
+
+ping = 0 == sub.run([PING_BIN, "-w1", "-W1", "-c1", DOMAIN if dns else IP],
+                    stdout=sub.DEVNULL).returncode
+
+bytes_per_sec = 0
+error = None
+if dns:
+    res = sub.run(["curl", "--silent", PROTOCOL + "://" + DOMAIN + FILE,
+                   "--max-time", str(CURL_TIMEOUT_SEC),
+                   "--write-out", "\n%{size_download} %{speed_download}"],
+                  stdout=sub.PIPE, stderr=sub.PIPE)
+    if res.returncode == 28:
+        error = "curl timed out after {} seconds".format(CURL_TIMEOUT_SEC)
+    elif res.returncode != 0:
+        sys.exit("download failed unexpectedly. curl outputs:\n{}".format(res.stderr))
+    else:
+        # the last line is the download speed
+        out = res.stdout.split(b"\n")[-1].strip().split(b" ")
+        try:
+            download_size = int(out[0])
+            if download_size != SIZE:
+                sys.exit("download size should have been {} but is {}"
+                         .format(SIZE, download_size))
+            bytes_per_sec = float(out[1])
+        except ValueError:
+            sys.exit("last line of curl was no float (bytes per sec), but:\n" +
+                     out[0:100] + "\nthere were " + len(out) + " lines in the output")
+
+# some yaml-like output
+def bool_(b):
+    return "true" if b else "false"
+
+print("---")
+print("version: 0.3")
+print("date: " + str(datetime.datetime.now()))
+print("ping-v4: " + bool_(v4))
+print("ping-v6: " + bool_(v6))
+print("dns: " + bool_(dns))
+print("download_speed: {}".format(int(bytes_per_sec)))
+# null or string
+print("error: " + repr(error))
+
+# version 0.2
+# + ping-v4 and ping-v6 fields
+# version 0.3
+# + error field (I want tagged unions …)
diff --git a/modules/user/openlab/stackenblocken.nix b/modules/user/openlab/stackenblocken.nix
new file mode 100644
index 00000000..421b64e9
--- /dev/null
+++ b/modules/user/openlab/stackenblocken.nix
@@ -0,0 +1,39 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.user.openlab.stackenblocken;
+  package = lib.getBin pkgs.vuizvui.openlab.stackenblocken.override {
+    volumePercent = cfg.volume;
+  };
+
+in
+{
+  options.vuizvui.user.openlab.stackenblocken = {
+    enable = mkEnableOption "STACKENBLOCKEN EVERY DAY";
+
+    volume = mkOption {
+      description = "Volume in percent";
+      default = 50;
+      # TODO: replace with types.intBetween https://github.com/NixOS/nixpkgs/pull/27239
+      type = types.addCheck types.int (x: x >= 0 && x <= 100);
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.user = {
+      services.stackenblocken = {
+        description = "stackenblocken timer";
+#        wantedBy = [ "default.target" ];
+        serviceConfig = {
+          ExecStart = "${package}/bin/stackenblocken";
+        };
+        # everyday at 21:45, except Wednesday (Yoga silence)
+        startAt = [ "Mon,Tue,Thu,Fri,Sat,Sun 21:45" "Wed 22:00" ];
+      };
+    };
+
+  };
+}
diff --git a/modules/user/profpatsch/programs/scanning.nix b/modules/user/profpatsch/programs/scanning.nix
new file mode 100644
index 00000000..6571246f
--- /dev/null
+++ b/modules/user/profpatsch/programs/scanning.nix
@@ -0,0 +1,29 @@
+{ config, pkgs, unfreeAndNonDistributablePkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.vuizvui.user.profpatsch.programs.scanning;
+
+in {
+  options.vuizvui.user.profpatsch.programs.scanning = {
+    enable = mkEnableOption "scanning &amp; simple-scan";
+
+    remoteScanners = mkOption {
+      type = lib.types.lines;
+      default = "";
+      description = ''
+        See <literal>hardware.sane.extraBackends</literal>.
+        Proxy, because I may want to change this option.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.simple-scan ];
+    hardware.sane = {
+      enable = true;
+      netConf = cfg.remoteScanners;
+      extraBackends = [ unfreeAndNonDistributablePkgs.hplipWithPlugin ];
+    };
+  };
+}
diff --git a/nixpkgs-path.nix b/nixpkgs-path.nix
new file mode 100644
index 00000000..acd3b37e
--- /dev/null
+++ b/nixpkgs-path.nix
@@ -0,0 +1,2 @@
+# This will be replaced by the channel expression generators in release.nix!
+toString <nixpkgs>
diff --git a/pkgs/aszlig/aacolorize/aacolorize.py b/pkgs/aszlig/aacolorize/aacolorize.py
new file mode 100755
index 00000000..ff19b687
--- /dev/null
+++ b/pkgs/aszlig/aacolorize/aacolorize.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+import os
+import sys
+
+from optparse import Option, OptionParser
+
+COLORS = {
+    "k": (30, "Black"),
+    "r": (31, "Red"),
+    "g": (32, "Green"),
+    "y": (33, "Yellow"),
+    "b": (34, "Blue"),
+    "p": (35, "Pink"),
+    "c": (36, "Cyan"),
+    "w": (37, "White"),
+}
+
+ESC = chr(27)
+
+class ColorizeError(Exception):
+    pass
+
+class Color(object):
+    def __init__(self, ident=None):
+        """
+        Initialize a color object, if no `ident` is given or it's invalid,
+        the Color object represents "no color".
+        """
+        if ident is not None:
+            spec = COLORS.get(ident.lower(), None)
+        else:
+            spec = None
+
+        if spec is None:
+            self.ident = None
+            self.bold = False
+            self.code = None
+            self.name = "None"
+        else:
+            self.ident = ident
+            self.code, self.name = spec
+
+            if ident.isupper():
+                self.bold = True
+            else:
+                self.bold = False
+
+    @property
+    def attrs(self):
+        """
+        A tuple consisting of the SGR attributes.
+        """
+        if self.ident is None:
+            return ()
+
+        if self.bold:
+            return (1, self.code)
+        else:
+            return (self.code,)
+
+    def sgr_attrs(self, *attrs):
+        """
+        Return the attributes specified by `attrs` formatted according
+        to the CSI specification.
+        """
+        return ';'.join(map(lambda c: str(c), attrs))
+
+    def sgr(self, *attrs):
+        """
+        Start Set Graphics Rendition
+        Return the CSI escape code for `attrs`.
+        """
+        return "%s[%sm" % (ESC, self.sgr_attrs(*attrs))
+
+    def sgr_start(self):
+        """
+        Start Set Graphics Rendition
+        Return the CSI start escape code for the current color.
+        """
+        return self.sgr(*self.attrs)
+
+    def sgr_stop(self):
+        """
+        Clear Set Graphics Rendition
+        """
+        return self.sgr()
+
+    def apply(self, value):
+        """
+        Apply the current color to the string in `value`.
+        """
+        return "%s%s%s" % (self.sgr_start(), value, self.sgr_stop())
+
+    def describe(self):
+        """
+        Return the description of the current color IN color :-)
+        """
+        fmt = "%c: <ESC>[%sm -> [%s]"
+        return fmt % (
+            self.ident,
+            self.sgr_attrs(*self.attrs),
+            self.apply(self.name)
+        )
+
+    def transform_to(self, new_color):
+        """
+        Return the CSI sequences needed to transform into `new_color`.
+        """
+        if self.ident is None and new_color.ident is not None:
+            return new_color.sgr_start()
+        elif self.ident is not None and new_color.ident is None:
+            return self.sgr_stop()
+        elif self.ident is None and new_color.ident is None:
+            return ''
+        elif self.code == new_color.code:
+            if not self.bold and new_color.bold:
+                return self.sgr(1)
+            elif self.bold and not new_color.bold:
+                return self.sgr(22)
+            elif self.bold == new_color.bold:
+                return ''
+        else:
+            if self.bold and new_color.bold:
+                return new_color.sgr(new_color.code)
+
+        return self.sgr_stop()+new_color.sgr_start()
+
+    def __repr__(self):
+        if self.bold:
+            return "<Bold color %s>" % self.name.lower()
+        else:
+            return "<Color %s>" % self.name.lower()
+
+def print_colortable():
+    for ident in COLORS.iterkeys():
+        normal = Color(ident).describe()
+        bold = Color(ident.upper()).describe()
+        sys.stdout.write("%-35s%s\n" % (normal, bold))
+
+def colorize_art(art, colmap):
+    if len(art) != len(colmap):
+        raise ColorizeError("Art and colormap differ in size!")
+
+    no_color = Color()
+
+    out = ""
+    last_color = no_color
+    for i, char in enumerate(colmap):
+        color = Color(char)
+        out += last_color.transform_to(color) + art[i]
+        last_color = color
+
+    last_color.transform_to(no_color)
+
+    return out
+
+def colorize_file(artfile, mapfile=None):
+    if mapfile is None:
+        mapfile = os.path.splitext(artfile)[0]+'.colmap'
+
+    asciiart = open(artfile, 'r').read()
+    colormap = open(mapfile, 'r').read()
+
+    return colorize_art(asciiart, colormap)
+
+if __name__ == "__main__":
+    parser = OptionParser(usage="%prog [options] artfile [mapfile]")
+    parser.add_option("-t", "--table", action="store_true", dest="table",
+                      help="Show color table and exit.")
+
+    (options, args) = parser.parse_args()
+
+    if options.table:
+        print_colortable()
+        parser.exit()
+
+    if not len(args) in (1, 2):
+        parser.print_help()
+        parser.exit()
+    else:
+        colorized = colorize_file(*args)
+        sys.stdout.write(colorized)
diff --git a/pkgs/aszlig/aacolorize/default.nix b/pkgs/aszlig/aacolorize/default.nix
new file mode 100644
index 00000000..ef36f4e0
--- /dev/null
+++ b/pkgs/aszlig/aacolorize/default.nix
@@ -0,0 +1,13 @@
+{ pythonPackages, runCommand }:
+
+pythonPackages.buildPythonPackage {
+  name = "aacolorize";
+  src = runCommand "aacolorize-src" {} ''
+    mkdir -p "$out"
+    cp "${./aacolorize.py}" "$out/aacolorize"
+    cat > "$out/setup.py" <<SETUP
+    from distutils.core import setup
+    setup(name='aacolorize', scripts=['aacolorize'])
+    SETUP
+  '';
+}
diff --git a/pkgs/aszlig/axbo/default.nix b/pkgs/aszlig/axbo/default.nix
new file mode 100644
index 00000000..fe503863
--- /dev/null
+++ b/pkgs/aszlig/axbo/default.nix
@@ -0,0 +1,53 @@
+{ stdenv, fetchurl, fetchFromGitHub, jdk, jre, ant, makeWrapper
+, commonsLogging, librxtx_java
+}:
+
+stdenv.mkDerivation rec {
+  name = "axbo-research-${version}";
+  version = "3.0.12";
+
+  src = fetchFromGitHub {
+    owner = "jansolo";
+    repo = "aXbo-research";
+    #rev = "aXbo-research_${version}";
+    # Includes MIT license:
+    rev = "6e6888917b5f200a44509650d6f46ec42c133cdc";
+    sha256 = "0nbyxajl75q80cnyl9c0sjlyk3rmhm7k8w8mksg4lfyh78ynayyc";
+  };
+
+  sourceRoot = "${src.name}/aXbo-research";
+
+  buildInputs = [ jdk ant makeWrapper ];
+
+  buildPhase = ''
+    ant -Dplatforms.JDK_1.7.home="$JAVA_HOME" jar
+  '';
+
+  extraJars = [
+    "commons-beanutils-1.8.3"
+    "commons-digester3-3.2"
+    "commons-io-2.4"
+    "jcommon-1.0.20"
+    "jfreechart-1.0.16"
+    "streamflyer-core-1.0.1"
+    "swingx-all-1.6.4"
+  ];
+
+  installPhase = with stdenv.lib; let
+    classpath = makeSearchPath "share/java/\\*" [
+      "$out"
+      commonsLogging
+      librxtx_java
+    ];
+  in ''
+    for dep in $extraJars; do
+      install -vD -m 644 "lib/$dep.jar" "$out/share/java/$dep.jar"
+    done
+    install -vD -m 644 dist/axbo.jar "$out/share/java/axbo.jar"
+
+    mkdir -p "$out/bin"
+    makeWrapper "${jre}/bin/java" "$out/bin/axbo-research" \
+      --add-flags "-Djava.library.path='${librxtx_java}/lib'" \
+      --add-flags "-cp ${classpath} com.dreikraft.axbo.Axbo"
+  '';
+}
diff --git a/pkgs/aszlig/default.nix b/pkgs/aszlig/default.nix
new file mode 100644
index 00000000..d1dbcb73
--- /dev/null
+++ b/pkgs/aszlig/default.nix
@@ -0,0 +1,15 @@
+{ callPackage, callPackage_i686, vim_configurable, xournal, gopass }:
+
+{
+  aacolorize = callPackage ./aacolorize { };
+  axbo = callPackage ./axbo { };
+  git-detach = callPackage ./git-detach { };
+  gopass = callPackage ./gopass { inherit gopass; };
+  grandpa = callPackage ./grandpa { };
+  librxtx_java = callPackage ./librxtx-java { };
+  lockdev = callPackage ./lockdev { };
+  psi = callPackage ./psi { };
+  pvolctrl = callPackage ./pvolctrl { };
+  vim = callPackage ./vim { vim = vim_configurable; };
+  xournal = callPackage ./xournal { inherit xournal; };
+}
diff --git a/pkgs/aszlig/git-detach/default.nix b/pkgs/aszlig/git-detach/default.nix
new file mode 100644
index 00000000..fb20843e
--- /dev/null
+++ b/pkgs/aszlig/git-detach/default.nix
@@ -0,0 +1,33 @@
+{ writeScriptBin, stdenv, git, coreutils, patch }:
+
+writeScriptBin "git-detach" ''
+  #!${stdenv.shell}
+
+  if [ $# -le 0 -o "$1" = "--help" -o "$1" = "-h" ]; then
+      echo "Usage: $0 COMMAND [ARGS...]" >&2
+      echo >&2
+      echo "Run COMMAND in a clean Git working directory" >&2
+      echo "without untracked files and .git directory." >&2
+      exit 1
+  fi
+
+  diffToHead="$("${git}/bin/git" diff HEAD)"
+
+  if tmpdir="$("${coreutils}/bin/mktemp" -d git-detach.XXXXXXXXXX)"; then
+    trap "rm -rf '${"\${tmpdir//\\'/\\'\\\\\\'\\'}"}'" EXIT
+    "${git}/bin/git" archive --format=tar HEAD | (
+      set -e
+      basedir="$tmpdir/$("${coreutils}/bin/basename" "$(pwd)")"
+      mkdir "$basedir"
+      cd "$basedir"
+      tar x
+      if [ -n "$diffToHead" ]; then
+        echo "$diffToHead" | "${patch}/bin/patch" -s -p1
+      fi
+      exec "$@"
+    )
+    exit $?
+  else
+    echo "Unable to create temporary directory!" >&2
+  fi
+''
diff --git a/pkgs/aszlig/gopass/ascii-symbols.patch b/pkgs/aszlig/gopass/ascii-symbols.patch
new file mode 100644
index 00000000..01365193
--- /dev/null
+++ b/pkgs/aszlig/gopass/ascii-symbols.patch
@@ -0,0 +1,17 @@
+diff --git a/pkg/tree/simple/tree.go b/pkg/tree/simple/tree.go
+index aa9f42a..76f56e9 100644
+--- a/pkg/tree/simple/tree.go
++++ b/pkg/tree/simple/tree.go
+@@ -8,9 +8,9 @@ import (
+ 
+ const (
+ 	symEmpty  = "    "
+-	symBranch = "├── "
+-	symLeaf   = "└── "
+-	symVert   = "│   "
++	symBranch = "|-- "
++	symLeaf   = "`-- "
++	symVert   = "|   "
+ )
+ 
+ var (
diff --git a/pkgs/aszlig/gopass/default.nix b/pkgs/aszlig/gopass/default.nix
new file mode 100644
index 00000000..9075a496
--- /dev/null
+++ b/pkgs/aszlig/gopass/default.nix
@@ -0,0 +1,8 @@
+{ gopass }:
+
+gopass.overrideAttrs (drv: {
+  patches = [
+    ./ascii-symbols.patch
+    ./use-color-in-pager.patch
+  ];
+})
diff --git a/pkgs/aszlig/gopass/use-color-in-pager.patch b/pkgs/aszlig/gopass/use-color-in-pager.patch
new file mode 100644
index 00000000..86f6fd53
--- /dev/null
+++ b/pkgs/aszlig/gopass/use-color-in-pager.patch
@@ -0,0 +1,20 @@
+diff --git a/pkg/action/list.go b/pkg/action/list.go
+index 03de22b..d54113f 100644
+--- a/pkg/action/list.go
++++ b/pkg/action/list.go
+@@ -14,7 +14,6 @@ import (
+ 	"github.com/justwatchcom/gopass/pkg/termutil"
+ 	"github.com/justwatchcom/gopass/pkg/tree"
+ 
+-	"github.com/fatih/color"
+ 	shellquote "github.com/kballard/go-shellquote"
+ 	"github.com/pkg/errors"
+ 	"github.com/urfave/cli"
+@@ -89,7 +88,6 @@ func redirectPager(ctx context.Context, subtree tree.Tree) (io.Writer, *bytes.Bu
+ 	if subtree == nil || subtree.Len() < rows {
+ 		return stdout, nil
+ 	}
+-	color.NoColor = true
+ 	buf := &bytes.Buffer{}
+ 	return buf, buf
+ }
diff --git a/pkgs/aszlig/grandpa/default.nix b/pkgs/aszlig/grandpa/default.nix
new file mode 100644
index 00000000..b4b5087e
--- /dev/null
+++ b/pkgs/aszlig/grandpa/default.nix
@@ -0,0 +1,20 @@
+{ fetchFromGitHub, pythonPackages, gpm }:
+
+pythonPackages.buildPythonPackage {
+  name = "grandpa-0.5";
+  namePrefix = "";
+
+  src = fetchFromGitHub {
+    owner = "aszlig";
+    repo = "GrandPA";
+    rev = "d8d2571f732a68ed18be7533244db2cfb822b4c1";
+    sha256 = "19zf3pnr1adngncvinvn8yyvc0sj66lp7lwiql6379rf78xxlmhn";
+  };
+
+  doCheck = false;
+
+  buildInputs = [ pythonPackages.cython gpm ];
+  propagatedBuildInputs = [ pythonPackages.pyserial ];
+
+  meta.platforms = [ "x86_64-linux" ];
+}
diff --git a/pkgs/aszlig/librxtx-java/default.nix b/pkgs/aszlig/librxtx-java/default.nix
new file mode 100644
index 00000000..1553a146
--- /dev/null
+++ b/pkgs/aszlig/librxtx-java/default.nix
@@ -0,0 +1,47 @@
+{ stdenv, fetchurl, fetchpatch, unzip, jdk, lockdev }:
+
+stdenv.mkDerivation rec {
+  name = "rxtx-${version}";
+  version = "2.2pre2";
+
+  src = fetchurl {
+    urls = [
+      "http://rxtx.qbang.org/pub/rxtx/${name}.zip"
+      "ftp://ftp.freebsd.org/pub/FreeBSD/ports/distfiles/${name}.zip"
+    ];
+    sha256 = "00sv9604hkq81mshih0fhqfzn4mf01d6rish6vplsi0gfqz3fc1w";
+  };
+
+  patches = let
+    baseurl = "https://sources.debian.net/data/main/"
+            + "r/rxtx/2.2pre2-13/debian/patches";
+  in [
+    (fetchpatch {
+      url = "${baseurl}/fhs_lock_buffer_overflow_fix.patch";
+      sha256 = "1v31q6ciy5v6bm5z8a1wssqn4nwvbcg4nnplgsvv1h8mzdq2832i";
+    })
+    (fetchpatch {
+      url = "${baseurl}/fix_snprintf.patch";
+      sha256 = "09r9jca0hb13bx85l348jkxnh1p0g5i0d6dnpm142vlwsj0d7afy";
+    })
+    (fetchpatch {
+      url = "${baseurl}/format_security.patch";
+      sha256 = "0adg7y9ak4xvgyswdhx6fsxq8jlb8y55xl3s6l0p8w0mfrhw7ysk";
+    })
+  ];
+
+  buildInputs = [ unzip jdk lockdev ];
+
+  NIX_CFLAGS_COMPILE = "-DUTS_RELEASE=\"3.8.0\"";
+
+  configureFlags = [ "--enable-liblock" ];
+
+  makeFlags = [
+    "JHOME=$(out)/share/java"
+    "RXTX_PATH=$(out)/lib"
+  ];
+
+  preInstall = ''
+    mkdir -p "$out/lib" "$out/share/java"
+  '';
+}
diff --git a/pkgs/aszlig/lockdev/default.nix b/pkgs/aszlig/lockdev/default.nix
new file mode 100644
index 00000000..52e78eb5
--- /dev/null
+++ b/pkgs/aszlig/lockdev/default.nix
@@ -0,0 +1,23 @@
+{ stdenv, fetchurl, perl }:
+
+let
+  baseurl = "ftp://ftp.debian.org/debian/pool/main/l/lockdev/";
+in stdenv.mkDerivation rec {
+  name = "lockdev-${version}";
+  version = "1.0.3";
+
+  buildInputs = [ perl ];
+
+  patches = stdenv.lib.singleton (fetchurl {
+    url = baseurl + "lockdev_1.0.3-1.5.diff.gz";
+    sha256 = "1l3pq1nfb5qx3i91cjaiz3c53368gw6m28a5mv9391n5gmsdmi3r";
+  });
+
+  NIX_CFLAGS_COMPILE = "-fPIC -D_PATH_LOCK=\"/tmp\"";
+  installFlags = [ "basedir=$(out)" ];
+
+  src = fetchurl {
+    url = baseurl + "lockdev_${version}.orig.tar.gz";
+    sha256 = "10lzhq6r2dn8y3ki7wlqsa8s3ndkf842bszcjw4dbzf3g9fn7bnc";
+  };
+}
diff --git a/pkgs/aszlig/psi/config.patch b/pkgs/aszlig/psi/config.patch
new file mode 100644
index 00000000..a37276fd
--- /dev/null
+++ b/pkgs/aszlig/psi/config.patch
@@ -0,0 +1,278 @@
+diff --git a/options/default.xml b/options/default.xml
+index 63fd2667..de3332ed 100644
+--- a/options/default.xml
++++ b/options/default.xml
+@@ -19,7 +19,7 @@
+             <domain comment="Always use the same domain to register with. Leave this empty to allow the user to choose his server." type="QString"/>
+         </account>
+         <auto-update comment="Auto updater">
+-            <check-on-startup comment="Check for available updates on startup" type="bool">true</check-on-startup>
++            <check-on-startup comment="Check for available updates on startup" type="bool">false</check-on-startup>
+         </auto-update>
+         <enable-multicast comment="Enable multicasting messages to multiple recipients" type="bool">false</enable-multicast>
+         <html comment="Hypertext markup options">
+@@ -89,7 +89,7 @@
+                 <security comment="Options related to the seciruty UI">
+                     <show comment="Show the security UI" type="bool">true</show>
+                 </security>
+-                <single comment="Limit the client to a single account" type="bool">false</single>
++                <single comment="Limit the client to a single account" type="bool">true</single>
+             </account>
+             <message comment="Message options">
+                 <enabled comment="Enable message (i.e. non-chat) functionality" type="bool">true</enabled>
+@@ -135,7 +135,7 @@ QWidget#bottomFrame>QWidget>QTextEdit[correction="true"] {
+                 <default-jid-mode comment="Default jid mode: barejid | auto" type="QString">auto</default-jid-mode>
+                 <default-jid-mode-ignorelist comment="Default autojid mode ignore list: jid1,jid2,..." type="QString"></default-jid-mode-ignorelist>
+                 <history comment="Message history options">
+-                    <preload-history-size comment="The number of preloaded messages" type="int">5</preload-history-size>
++                    <preload-history-size comment="The number of preloaded messages" type="int">10</preload-history-size>
+                 </history>
+             </chat>
+             <save>
+@@ -153,8 +153,8 @@ QWidget#bottomFrame>QWidget>QTextEdit[correction="true"] {
+                 <auto-delete-unlisted comment="Automatically remove an unlisted contact from the contact list if it does not have any pending messages anymore" type="bool">false</auto-delete-unlisted>
+                 <opacity comment="Opacity percentage of the contact list" type="int">100</opacity>
+                 <status-messages comment="Status messages for contacts">
+-                    <single-line comment="Show status messages on the same line as the nickname" type="bool">true</single-line>
+-                    <show comment="Show status messages" type="bool">false</show>
++                    <single-line comment="Show status messages on the same line as the nickname" type="bool">false</single-line>
++                    <show comment="Show status messages" type="bool">true</show>
+                 </status-messages>
+                 <tooltip comment="Display options for the contact list tooltips">
+                     <css type="QString"></css>
+@@ -215,7 +215,7 @@ QLineEdit#le_status_text {
+                 <always-on-top type="bool">false</always-on-top>
+                 <automatically-resize-roster type="bool">false</automatically-resize-roster>
+                 <grow-roster-upwards type="bool">true</grow-roster-upwards>
+-                <disable-scrollbar type="bool">true</disable-scrollbar>
++                <disable-scrollbar type="bool">false</disable-scrollbar>
+                 <contact-sort-style type="QString">status</contact-sort-style>
+                 <disable-service-discovery type="bool">false</disable-service-discovery>
+                 <enable-groups type="bool">true</enable-groups>
+@@ -230,7 +230,7 @@ QLineEdit#le_status_text {
+                     <agent-contacts type="bool">true</agent-contacts>
+                     <away-contacts type="bool">true</away-contacts>
+                     <hidden-contacts-group type="bool">true</hidden-contacts-group>
+-                    <offline-contacts type="bool">true</offline-contacts>
++                    <offline-contacts type="bool">false</offline-contacts>
+                     <self-contact type="bool">true</self-contact>
+                 </show>
+                 <show-group-counts type="bool">true</show-group-counts>
+@@ -255,7 +255,7 @@ QLineEdit#le_status_text {
+                 <use-left-click type="bool">false</use-left-click>
+                 <use-single-click type="bool">false</use-single-click>
+                 <use-status-change-animation type="bool">true</use-status-change-animation>
+-                <aio-left-roster type="bool">false</aio-left-roster>
++                <aio-left-roster type="bool">true</aio-left-roster>
+                 <use-transport-icons type="bool">true</use-transport-icons>
+                 <saved-window-geometry type="QRect" >
+                     <x>64</x>
+@@ -292,7 +292,7 @@ QLineEdit#le_status_text {
+                     <custom-pgp-key comment="Show the 'assign pgp key' menu" type="bool">true</custom-pgp-key>
+                 </contact>
+                 <main comment="Options for the main menu">
+-                    <change-profile comment="Show the 'change profile' menu" type="bool">true</change-profile>
++                    <change-profile comment="Show the 'change profile' menu" type="bool">false</change-profile>
+                 </main>
+                 <status comment="Options for the status menu">
+                     <chat comment="Enable free for chat" type="bool">true</chat>
+@@ -343,7 +343,7 @@ QLineEdit#le_status_text {
+             <disable-send-button type="bool">true</disable-send-button>
+             <systemtray comment="Options related to the system tray">
+                 <use-old comment="Use the old system tray code (deprecated)" type="bool">false</use-old>
+-                <enable type="bool">true</enable>
++                <enable type="bool">false</enable>
+                 <use-double-click type="bool">false</use-double-click>
+             </systemtray>
+             <flash-windows comment="Allow windows to flash upon activity" type="bool">true</flash-windows>
+@@ -361,8 +361,8 @@ QLineEdit#le_status_text {
+                     <contactlist>
+                         <background type="QColor"/>
+                         <grouping>
+-                            <header-background type="QColor">#f0f0f0</header-background>
+-                            <header-foreground type="QColor">#5a5a5a</header-foreground>
++                            <header-background type="QColor">#00007f</header-background>
++                            <header-foreground type="QColor">#969696</header-foreground>
+                         </grouping>
+                         <profile>
+                             <header-background type="QColor">#969696</header-background>
+@@ -372,16 +372,16 @@ QLineEdit#le_status_text {
+                             <away type="QColor">#004bb4</away>
+                             <do-not-disturb type="QColor">#7e0000</do-not-disturb>
+                             <offline type="QColor">#646464</offline>
+-                            <online type="QColor"/>
++                            <online type="QColor">#ffffff</online>
+                         </status>
+-                        <status-change-animation1 type="QColor">#000000</status-change-animation1>
++                        <status-change-animation1 type="QColor">#6f0000</status-change-animation1>
+                         <status-change-animation2 type="QColor">#969696</status-change-animation2>
+                         <status-messages type="QColor">#808080</status-messages>
+                     </contactlist>
+                     <tooltip>
+                         <enable comment="Enable tooltip coloring feature" type="bool">true</enable>
+                         <background comment="Tooltip background color" type="QColor">#e9ecc7</background>
+-                        <text comment="Tooltip text color" type="QColor">#000000</text>
++                        <text comment="Tooltip text color" type="QColor">#ffffff</text>
+                     </tooltip>
+                     <muc>
+                         <nick-colors type="QStringList" >
+@@ -392,21 +392,21 @@ QLineEdit#le_status_text {
+                             <item>Red</item>
+                         </nick-colors>
+                         <role-moderator type="QColor">#910000</role-moderator>
+-                        <role-participant type="QColor">#00008a</role-participant>
++                        <role-participant type="QColor">#00aaff</role-participant>
+                         <role-visitor type="QColor">#336600</role-visitor>
+-                        <role-norole type="QColor">black</role-norole>
++                        <role-norole type="QColor">#cccccc</role-norole>
+                     </muc>
+                     <messages comment="Message coloring.">
+-                        <received type="QColor" comment="Color used to indicate received messages.">#0000ff</received>
++                        <received type="QColor" comment="Color used to indicate received messages.">#0055ff</received>
+                         <sent type="QColor" comment="Color used to indicate sent messages.">#ff0000</sent>
+                         <informational type="QColor" comment="Color used to indicate informational (status change, spooled) messages.">#008000</informational>
+                         <usertext type="QColor" comment="Color used to indicate additional text for informational messages.">#606060</usertext>
+                         <highlighting type="QColor">#FF0000</highlighting>
+-                        <link type="QColor">#000080</link>
++                        <link type="QColor">#55ffff</link>
+                         <link-visited type="QColor">#400080</link-visited>
+                     </messages>
+                     <chat>
+-                        <composing-color type="QColor">darkGreen</composing-color>
++                        <composing-color type="QColor">#cccccc</composing-color>
+                         <unread-message-color type="QColor">red</unread-message-color>
+                         <inactive-color type="QColor">grey</inactive-color>
+                     </chat>
+@@ -419,10 +419,10 @@ QLineEdit#le_status_text {
+                     <use-slim-group-headings type="bool">false</use-slim-group-headings>
+                 </contactlist>
+                 <font>
+-                    <chat type="QString">Sans Serif,11,-1,5,50,0,0,0,0,0</chat>
+-                    <contactlist type="QString">Sans Serif,11,-1,5,50,0,0,0,0,0</contactlist>
+-                    <message type="QString">Sans Serif,11,-1,5,50,0,0,0,0,0</message>
+-                    <passive-popup type="QString">Sans Serif,9,-1,5,50,0,0,0,0,0</passive-popup>
++                    <chat type="QString">Monospace,12,-1,5,50,0,0,0,0,0</chat>
++                    <contactlist type="QString">Monospace,12,-1,5,50,0,0,0,0,0</contactlist>
++                    <message type="QString">Monospace,12,-1,5,50,0,0,0,0,0</message>
++                    <passive-popup type="QString">Monospace,12,-1,5,50,0,0,0,0,0</passive-popup>
+                 </font>
+                 <css type="QString" />
+             </look>
+@@ -470,20 +470,20 @@ QLineEdit#le_status_text {
+                     <suppress-while-away type="bool">false</suppress-while-away>
+                 </popup-dialogs>
+                 <sounds>
+-                    <chat-message type="QString">sound/chat2.wav</chat-message>
+-                    <groupchat-message type="QString">sound/chat2.wav</groupchat-message>
+-                    <completed-file-transfer type="QString">sound/ft_complete.wav</completed-file-transfer>
+-                    <contact-offline type="QString">sound/offline.wav</contact-offline>
+-                    <contact-online type="QString">sound/online.wav</contact-online>
+-                    <enable type="bool">true</enable>
+-                    <incoming-file-transfer type="QString">sound/ft_incoming.wav</incoming-file-transfer>
+-                    <incoming-headline type="QString">sound/chat2.wav</incoming-headline>
+-                    <incoming-message type="QString">sound/chat2.wav</incoming-message>
+-                    <new-chat type="QString">sound/chat1.wav</new-chat>
++                    <chat-message type="QString"/>
++                    <groupchat-message type="QString"/>
++                    <completed-file-transfer type="QString"/>
++                    <contact-offline type="QString"/>
++                    <contact-online type="QString"/>
++                    <enable type="bool">false</enable>
++                    <incoming-file-transfer type="QString"/>
++                    <incoming-headline type="QString"/>
++                    <incoming-message type="QString"/>
++                    <new-chat type="QString"/>
+                     <notify-every-muc-message type="bool">false</notify-every-muc-message>
+-                    <outgoing-chat type="QString">sound/send.wav</outgoing-chat>
+-                    <silent-while-away type="bool">false</silent-while-away>
+-                    <system-message type="QString">sound/chat2.wav</system-message>
++                    <outgoing-chat type="QString"/>
++                    <silent-while-away type="bool">true</silent-while-away>
++                    <system-message type="QString"/>
+                     <unix-sound-player type="QString"/>
+                 </sounds>
+                 <successful-subscription type="bool">true</successful-subscription>
+@@ -502,7 +502,7 @@ QLineEdit#le_status_text {
+                 <mouse-middle-button type="QString">close</mouse-middle-button> <!-- hide|close|detach -->
+                 <mouse-doubleclick-action type="QString">detach</mouse-doubleclick-action>
+                 <size type="QString"></size> <!-- will be invalid when converted to QSize so we can detect first load -->
+-                <grouping type="QString" comment="A ':' seperated list of groups of kinds of tabs to keep in the same tabset. 'C' for chat and 'M' for mucs. 'A' means using all in one window patch.">CM</grouping>
++                <grouping type="QString" comment="A ':' seperated list of groups of kinds of tabs to keep in the same tabset. 'C' for chat and 'M' for mucs. 'A' means using all in one window patch.">ACM</grouping>
+                 <group-state comment="Saved state data of the tabsets defined by options.ui.tabs.grouping"/>
+                 <tab-singles type="QString" comment="Tab types that would have been untabbed are given their own tabset. 'C' for chat and 'M' for mucs"/>
+                 <use-tab-shortcuts type="bool">true</use-tab-shortcuts>
+@@ -715,7 +715,7 @@ QLineEdit#le_status_text {
+             <last-activity type="bool">true</last-activity>
+         </service-discovery>
+         <status>
+-            <ask-for-message-on-offline type="bool">false</ask-for-message-on-offline>
++            <ask-for-message-on-offline type="bool">true</ask-for-message-on-offline>
+             <ask-for-message-on-online type="bool">false</ask-for-message-on-online>
+             <ask-for-message-on-chat type="bool">true</ask-for-message-on-chat>
+             <ask-for-message-on-away type="bool">true</ask-for-message-on-away>
+@@ -738,7 +738,20 @@ QLineEdit#le_status_text {
+                 <by-template type="bool">true</by-template>
+                 <by-status type="bool">false</by-status>
+             </last-overwrite>
+-            <presets/>
++            <presets>
++                <m0>
++                    <key type="QString">zone</key>
++                    <force-priority type="bool">false</force-priority>
++                    <status type="QString">dnd</status>
++                    <message type="QString">In The Zone[TM]</message>
++                </m0>
++                <m1>
++                    <key type="QString">sleep</key>
++                    <force-priority type="bool">false</force-priority>
++                    <status type="QString">offline</status>
++                    <message type="QString">Sleeping the hell out of here.</message>
++                </m1>
++            </presets>
+             <presets-in-status-menus type="QString" comment="'yes', 'no' or 'submenu'">submenu</presets-in-status-menus>
+             <show-only-online-offline type="bool">false</show-only-online-offline>
+             <show-choose type="bool">true</show-choose>
+@@ -778,5 +791,9 @@ QLineEdit#le_status_text {
+         </keychain>
+     </options>
+     <accounts comment="Account definitions and options"/>
+-    <plugins comment="Plugin options"/>
++    <plugins comment="Plugin options">
++        <auto-load>
++            <omemo type="bool">true</omemo>
++        </auto-load>
++    </plugins>
+ </psi>
+diff --git a/src/psi_profiles.cpp b/src/psi_profiles.cpp
+index 894c8ac9..a6798e76 100644
+--- a/src/psi_profiles.cpp
++++ b/src/psi_profiles.cpp
+@@ -75,8 +75,8 @@ void UserAccount::reset()
+     req_mutual_auth           = false;
+     legacy_ssl_probe          = false;
+     security_level            = QCA::SL_None;
+-    ssl                       = SSL_Auto;
+-    jid                       = "";
++    ssl                       = SSL_Yes;
++    jid                       = "@jid@";
+     pass                      = "";
+     scramSaltedHashPassword   = "";
+     opt_pass                  = false;
+@@ -86,7 +86,7 @@ void UserAccount::reset()
+     opt_automatic_resource    = true;
+     priority_dep_on_status    = true;
+     ignore_global_actions     = false;
+-    resource                  = ApplicationInfo::name();
++    resource                  = "@resource@";
+     priority                  = 55;
+     ibbOnly                   = false;
+     opt_keepAlive             = true;
+@@ -129,7 +129,7 @@ void UserAccount::reset()
+               << "stun.voipbuster.com"
+               << "stun.voxgratia.org";
+ 
+-    stunHost = stunHosts[0];
++    stunHost = "";
+ 
+     keybind.clear();
+ 
diff --git a/pkgs/aszlig/psi/darkstyle.patch b/pkgs/aszlig/psi/darkstyle.patch
new file mode 100644
index 00000000..4e3a1296
--- /dev/null
+++ b/pkgs/aszlig/psi/darkstyle.patch
@@ -0,0 +1,32 @@
+diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
+index 7118ea75..c6f58e35 100644
+--- a/src/CMakeLists.txt
++++ b/src/CMakeLists.txt
+@@ -237,6 +237,7 @@ endif()
+ set(RESOURCES
+     ${PROJECT_SOURCE_DIR}/psi.qrc
+     ${ICONSETSQRC_OUTPUT_FILE}
++    ${QDARKSTYLE_PATH}/qdarkstyle/style.qrc
+ )
+ qt5_add_resources(QRC_SOURCES ${RESOURCES})
+ 
+diff --git a/src/main.cpp b/src/main.cpp
+index b45fbab0..1cbead4a 100644
+--- a/src/main.cpp
++++ b/src/main.cpp
+@@ -532,6 +532,15 @@ PSI_EXPORT_FUNC int main(int argc, char *argv[])
+     QCoreApplication::addLibraryPath(appPath);
+ # endif
+     PsiApplication app(argc, argv);
++
++    QFile darkstyle(":qdarkstyle/style.qss");
++    if (!darkstyle.exists()) {
++        qWarning() << "Unable to set dark style";
++    } else {
++        darkstyle.open(QFile::ReadOnly | QFile::Text);
++        QTextStream ts(&darkstyle);
++        app.setStyleSheet(ts.readAll());
++    }
+     QApplication::setApplicationName(ApplicationInfo::name());
+     QApplication::addLibraryPath(ApplicationInfo::resourcesDir());
+     QApplication::addLibraryPath(ApplicationInfo::homeDir(ApplicationInfo::DataLocation));
diff --git a/pkgs/aszlig/psi/default.nix b/pkgs/aszlig/psi/default.nix
new file mode 100644
index 00000000..d2e43d76
--- /dev/null
+++ b/pkgs/aszlig/psi/default.nix
@@ -0,0 +1,73 @@
+{ stdenv, lib, fetchFromGitHub, cmake, makeWrapper
+, hunspell, libgcrypt, libgpgerror, libidn, libotr, libsForQt5
+, libsignal-protocol-c, libtidy, qt5
+
+, substituteAll
+
+, jid ? "something@example.org"
+, resource ? "psi-aszlig"
+}:
+
+let
+  qdarkstyle = fetchFromGitHub {
+    owner = "ColinDuquesnoy";
+    repo = "QDarkStyleSheet";
+    rev = "c92d0c4c996e3e859134492e0f9f7f74bd0e12cd";
+    sha256 = "1qrmp3ibvgzwh2v1qfrfh8xiwvj0kbhj1bm17bjx7zpmnb8byz3m";
+  };
+
+in stdenv.mkDerivation rec {
+  name = "psi-${version}";
+  version = "2.0git20200208aszlig";
+
+  src = fetchFromGitHub {
+    owner = "psi-im";
+    repo = "psi";
+    rev = "f1ca4cc0d45d0c1981fd2abd5da40182bbd8c5fb";
+    sha256 = "170g3dlpd8hp9g4j4y28l8y2xhgsmfay4m7dknvd9vanxd7s42ks";
+    fetchSubmodules = true;
+  };
+
+  plugins = fetchFromGitHub {
+    owner = "psi-im";
+    repo = "plugins";
+    rev = "5dc21909fc46c4780e1f4d23c56bf4be94802912";
+    sha256 = "0bxlsmwisc22m8y0py1ms69fyqspyx1a1zcjh6m51c4vmzskfr7a";
+  };
+
+  patches = [
+    ./disable-xep-0232.patch
+    ./darkstyle.patch
+    ./disable-jingle.patch
+    (substituteAll {
+      src = ./config.patch;
+      inherit jid resource;
+    })
+  ];
+
+  preConfigure = ''
+    cp --no-preserve=all -rt src/plugins "$plugins"/*
+  '';
+
+  cmakeFlags = [
+    "-DENABLE_PLUGINS=ON" "-DUSE_KEYCHAIN=OFF" "-DPSI_VERSION=${version}"
+    "-DQDARKSTYLE_PATH=${qdarkstyle}"
+  ];
+
+  enableParallelBuilding = true;
+  nativeBuildInputs = [ cmake makeWrapper qt5.wrapQtAppsHook ];
+  buildInputs = [
+    hunspell
+    libgcrypt
+    libgpgerror
+    libidn
+    libotr
+    libsForQt5.qca-qt5
+    libsignal-protocol-c
+    libtidy
+    qt5.qtbase
+    qt5.qtmultimedia
+    qt5.qtwebengine
+    qt5.qtx11extras
+  ];
+}
diff --git a/pkgs/aszlig/psi/disable-jingle.patch b/pkgs/aszlig/psi/disable-jingle.patch
new file mode 100644
index 00000000..d76b9451
--- /dev/null
+++ b/pkgs/aszlig/psi/disable-jingle.patch
@@ -0,0 +1,12 @@
+diff --git a/iris/src/xmpp/xmpp-im/jingle.cpp b/iris/src/xmpp/xmpp-im/jingle.cpp
+index 0ac149a..e445acf 100644
+--- a/iris/src/xmpp/xmpp-im/jingle.cpp
++++ b/iris/src/xmpp/xmpp-im/jingle.cpp
+@@ -1681,6 +1681,7 @@ namespace XMPP { namespace Jingle {
+ 
+     Session *Manager::incomingSessionInitiate(const Jid &from, const Jingle &jingle, const QDomElement &jingleEl)
+     {
++        return nullptr;
+         if (d->maxSessions > 0 && d->sessions.size() == d->maxSessions) {
+             d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Wait, XMPP::Stanza::Error::ResourceConstraint);
+             return nullptr;
diff --git a/pkgs/aszlig/psi/disable-xep-0232.patch b/pkgs/aszlig/psi/disable-xep-0232.patch
new file mode 100644
index 00000000..80c8a385
--- /dev/null
+++ b/pkgs/aszlig/psi/disable-xep-0232.patch
@@ -0,0 +1,50 @@
+diff --git a/iris/src/xmpp/xmpp-im/client.cpp b/iris/src/xmpp/xmpp-im/client.cpp
+index 66960b0..c8edd75 100644
+--- a/iris/src/xmpp/xmpp-im/client.cpp
++++ b/iris/src/xmpp/xmpp-im/client.cpp
+@@ -1131,45 +1131,6 @@ DiscoItem Client::makeDiscoResult(const QString &node) const
+ 
+     item.setFeatures(features);
+ 
+-    // xep-0232 Software Information
+-    XData            si;
+-    XData::FieldList si_fields;
+-
+-    XData::Field si_type_field;
+-    si_type_field.setType(XData::Field::Field_Hidden);
+-    si_type_field.setVar("FORM_TYPE");
+-    si_type_field.setValue(QStringList(QLatin1String("urn:xmpp:dataforms:softwareinfo")));
+-    si_fields.append(si_type_field);
+-
+-    XData::Field software_field;
+-    software_field.setType(XData::Field::Field_TextSingle);
+-    software_field.setVar("software");
+-    software_field.setValue(QStringList(d->clientName));
+-    si_fields.append(software_field);
+-
+-    XData::Field software_v_field;
+-    software_v_field.setType(XData::Field::Field_TextSingle);
+-    software_v_field.setVar("software_version");
+-    software_v_field.setValue(QStringList(d->clientVersion));
+-    si_fields.append(software_v_field);
+-
+-    XData::Field os_field;
+-    os_field.setType(XData::Field::Field_TextSingle);
+-    os_field.setVar("os");
+-    os_field.setValue(QStringList(d->osName));
+-    si_fields.append(os_field);
+-
+-    XData::Field os_v_field;
+-    os_v_field.setType(XData::Field::Field_TextSingle);
+-    os_v_field.setVar("os_version");
+-    os_v_field.setValue(QStringList(d->osVersion));
+-    si_fields.append(os_v_field);
+-
+-    si.setType(XData::Data_Result);
+-    si.setFields(si_fields);
+-
+-    item.setExtensions(QList<XData>() << si);
+-
+     return item;
+ }
+ 
diff --git a/pkgs/aszlig/pvolctrl/default.nix b/pkgs/aszlig/pvolctrl/default.nix
new file mode 100644
index 00000000..5701c19e
--- /dev/null
+++ b/pkgs/aszlig/pvolctrl/default.nix
@@ -0,0 +1,35 @@
+{ stdenv, fetchurl, pkgconfig, libpulseaudio }:
+
+stdenv.mkDerivation rec {
+  name = "pvolctrl-0.23";
+
+  unpackPhase = let
+    baseurl = "https://sites.google.com/site/guenterbartsch/blog/"
+            + "volumecontrolutilityforpulseaudio/";
+    makefile = fetchurl {
+      url = baseurl + "Makefile";
+      sha256 = "0l2ffvb617csk6h29y64v6ywhpcp7la6vvcip1w4nq0yry6jhrqz";
+    };
+    source = fetchurl {
+      url = baseurl + "pvolctrl.c";
+      sha256 = "0vcd5dlw9l47jpabwmmzdvlkn67fz55dr3sryyh56sl263mibjda";
+    };
+  in ''
+    mkdir -p "${name}"
+    sed -e 's|/usr/bin/||' "${makefile}" > "${name}/Makefile"
+    sed -e 's/PA_VOLUME_MAX/PA_VOLUME_NORM/
+    /avg_vol += (avg_vol \* vol_mod) \/ 100;/ {
+      s/(avg_vol/((int)PA_VOLUME_NORM/
+    }
+    /if (vol_mod)/i \
+      if (info->name == NULL || strncmp(info->name, "combined", 8) != 0) \
+        return;' "${source}" > "${name}/pvolctrl.c"
+    sourceRoot="${name}"
+  '';
+
+  installPhase = ''
+    install -D -T pvolctrl "$out/bin/pvolctrl"
+  '';
+
+  buildInputs = [ pkgconfig libpulseaudio ];
+}
diff --git a/pkgs/aszlig/vim/default.nix b/pkgs/aszlig/vim/default.nix
new file mode 100644
index 00000000..2e5e580f
--- /dev/null
+++ b/pkgs/aszlig/vim/default.nix
@@ -0,0 +1,523 @@
+{ stdenv, lib, fetchurl, fetchFromGitHub, writeText, writeTextFile, writeScript
+, python3Packages, ledger, meson, vim
+}:
+
+let
+  fetchVimScript = { srcId, sha256, type, name }: let
+    baseUrl = "http://www.vim.org/scripts/download_script.php";
+    src = fetchurl {
+      name = "script${toString srcId}.vim";
+      url = "${baseUrl}?src_id=${toString srcId}";
+      inherit sha256;
+    };
+  in stdenv.mkDerivation {
+    name = "vim-${type}-${toString srcId}";
+    buildCommand = ''
+      install -vD -m 0644 "${src}" "$out/${type}/${name}.vim"
+    '';
+  };
+
+  extractSubdir = subdir: src: stdenv.mkDerivation {
+    name = "${src.name}-subdir";
+    phases = [ "unpackPhase" "installPhase" ];
+    inherit src;
+    installPhase = ''
+      cp -Rd "${subdir}" "$out"
+    '';
+  };
+
+  pluginDeps = {
+    vimAddonMwUtils = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-mw-utils";
+      rev = "0c5612fa31ee434ba055e21c76f456244b3b5109";
+      sha256 = "147s1k4n45d3x281vj35l26sv4waxjlpqdn83z3k9n51556h1d45";
+    };
+
+    vimAddonCompletion = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-completion";
+      rev = "80f717d68df5b0d7b32228229ddfd29c3e86e435";
+      sha256 = "08acffzy847w8b5j8pdw6qsidm2859ki5q351n4r7fkr969p80mi";
+    };
+
+    vimAddonActions = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-actions";
+      rev = "a5d20500fb8812958540cf17862bd73e7af64936";
+      sha256 = "1wfkwr89sn2w97i94d0dqylcg9mr6pirjadi0a4l492nfnsh99bc";
+    };
+
+    vimAddonBackgroundCmd = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-background-cmd";
+      rev = "14df72660a95804a57c02b9ff0ae3198608e2491";
+      sha256 = "09lh6hqbx05gm7njhpqvhqdwig3pianq9rddxmjsr6b1vylgdgg4";
+    };
+
+    vimAddonErrorFormats = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-errorformats";
+      rev = "dcbb203ad5f56e47e75fdee35bc92e2ba69e1d28";
+      sha256 = "159zqm69fxbxcv3b2y99g57bf20qrzsijcvb5rzy2njxah3049m1";
+    };
+
+    vimAddonToggleBuffer = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-toggle-buffer";
+      rev = "a1b38b9c5709cba666ed2d84ef06548f675c6b0b";
+      sha256 = "1xq38kfdm36c34ln66znw841q797w5gm8bpq1x64bsf2h6n3ml03";
+    };
+
+    vimAddonGotoThingAtCursor = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-goto-thing-at-cursor";
+      rev = "f052e094bdb351829bf72ae3435af9042e09a6e4";
+      sha256 = "1ksm2b0j80zn8sz2y227bpcx4jsv76lwgr2gpgy2drlyqhn2vlv0";
+    };
+
+    vimAddonViews = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-views";
+      rev = "d1383ad56d0a07d7350880adbadf9de501729fa8";
+      sha256 = "09gqh7w5rk4lmra706schqaj8dnisf396lpsipm7xv6gy1qbslnv";
+    };
+
+    vimAddonSwfMill = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-swfmill";
+      rev = "726777e02cbe3ad8f82e37421fb37674f446a148";
+      sha256 = "0ablzl5clgfzhzwvzzbaj0cda0b4cyrj3pbv02f26hx7rfnssaqm";
+    };
+
+    tlib = fetchFromGitHub {
+      owner = "tomtom";
+      repo = "tlib_vim";
+      rev = "bc4097bd38c4bc040fe1e74df68dec6c9adfcb6a";
+      sha256 = "19v7bgmkk4k2g1z62bd0kky29xxfq96l7wfrl27wb2zijlhbrnpz";
+    };
+
+    vamStub = writeTextFile {
+      name = "vam-stub";
+      destination = "/autoload/vam.vim";
+      text = ''
+        fun! vam#DefineAndBind(local, global, default)
+          return ' if !exists('.string(a:global).') |
+                 \ let '.a:global.' = '.a:default.' |
+                 \ endif | let '.a:local.' = '.a:global
+        endfun
+      '';
+    };
+
+    pug = fetchFromGitHub {
+      owner = "digitaltoad";
+      repo = "vim-pug";
+      rev = "ddc5592f8c36bf4bd915c16b38b8c76292c2b975";
+      sha256 = "069pha18g1nlzg44k742vjxm4zwjd1qjzhfllkr35qaiflvjm84y";
+    };
+
+    scss = fetchFromGitHub {
+      owner = "cakebaker";
+      repo = "scss-syntax.vim";
+      rev = "4461789d02f81fd328afbdf27d6404b6c763c25f";
+      sha256 = "0d227d2c1pvcksk2njzpkgmxivrnfb0apn2r62q7q89s61ggbzfj";
+    };
+
+    less = fetchFromGitHub {
+      owner = "groenewege";
+      repo = "vim-less";
+      rev = "6e818d5614d5fc18d95a48c92b89e6db39f9e3d6";
+      sha256 = "0rhqcdry8ycnfbg534q4b3hm78an7mnqhiazxik7k08a57dk9dbm";
+    };
+  };
+
+  plugins = pluginDeps // {
+    vaxe = fetchFromGitHub {
+      owner = "jdonaldson";
+      repo = "vaxe";
+      rev = "d5f905f806c7c90bb116d4b06a78924341840021";
+      sha256 = "0axvavzxbi3m4shva1m0cm6finl1i2rwqgn6lnklxnr2g9sfi4j7";
+      extraPostFetch = ''
+        # Do not highlight ',' and ';'.
+        sed -i -e '/\<haxeOperator2\>/d' "$out/syntax/haxe.vim"
+      '';
+    };
+
+    factor = extractSubdir "misc/vim" (fetchFromGitHub {
+      owner = "slavapestov";
+      repo = "factor";
+      rev = "0d6f70cc7cf35cc627ee78886e2932091a651fe6";
+      sha256 = "0lmqzvrmwgmxpcpwgn59y033sf4jybmw3lffbjwww5d7ch90333q";
+    });
+
+    opaLang = extractSubdir "tools/editors/vim" (fetchFromGitHub {
+      owner = "MLstate";
+      repo = "opalang";
+      rev = "94e4e6d9d8da9a72214f4f28dd1ffa1a987997eb";
+      sha256 = "0d6b67868cfqakkz63y5ynpz549lbpfzc3c3x7kx3ffsv10xy3bb";
+    });
+
+    lslvim = fetchFromGitHub {
+      owner = "sukima";
+      repo = "LSLvim";
+      rev = "f269de39a1c713a43470e90d0ec78208c0f05e0b";
+      sha256 = "1plwx5id3jsj4y6yhshlf3rishxhf1b9k47g2cpzaczvqb5bl40w";
+    };
+
+    vimSyntaxShakespeare = fetchFromGitHub {
+      owner = "pbrisbin";
+      repo = "vim-syntax-shakespeare";
+      rev = "29085ae94ee3dbd7f39f2a7705d86692ef5bc365";
+      sha256 = "0kvys81jiwqzwmpbk1lvbciw28yha4shd1xby5saiy4b68l6d8rk";
+    };
+
+    glsl = fetchVimScript {
+      name = "glsl";
+      srcId = 3194;
+      sha256 = "1vqfcpjmfyjc95wns3i84kgd1k5r2lwjjvjcprygi9g9vng7i5xc";
+      type = "syntax";
+    };
+
+    actionScript = fetchVimScript {
+      name = "actionscript";
+      srcId = 1205;
+      sha256 = "0pdzqg678lhn7lmqf3z9icpj6ff2nnghsxy983kxkn8sblnzlhfs";
+      type = "syntax";
+    };
+
+    indentPython = fetchVimScript {
+      name = "python";
+      srcId = 4316;
+      sha256 = "1pgdiaqd1hm0qpspy1asj7i103pq0846lnjrxvl6pk17ymww9pmk";
+      type = "indent";
+    };
+
+    nixAddon = stdenv.mkDerivation {
+      name = "vim-nix-support";
+
+      lnl7 = fetchFromGitHub {
+        owner = "LnL7";
+        repo = "vim-nix";
+        rev = "be0c6bb409732b79cc86c177ca378b0b334e1efe";
+        sha256 = "1ivkwlm6lz43xk1m7aii0bgn2p3225dixck0qyhxw4zxhp2xiz06";
+      };
+
+      src = fetchFromGitHub {
+        owner = "MarcWeber";
+        repo = "vim-addon-nix";
+        rev = "3001a9db5f816dd7af11384f15415bddd146ef86";
+        sha256 = "195z2yz09wirpqjpsha8x7qcr9is1q8qph4j0svws6qbqrkh8ryy";
+      };
+
+      phases = [ "unpackPhase" "patchPhase" "installPhase" ];
+      patchPhase = ''
+        for what in indent syntax; do
+          install -vD -m 0644 "$lnl7/$what/nix.vim" "$what/nix.vim"
+        done
+
+        sed -i -re '/^ *au(group)? /,/^ *au(group)? +end/ {
+          s/^ *au(tocmd)? +((BufRead|BufNewFile),?)+ +[^ ]+ +setl(ocal)?/${
+            "& sw=2 sts=2 et iskeyword+='\\''"
+          }/
+        }' plugin/vim-addon-nix.vim
+
+        sed -n -e '/^ *setlocal/ {
+          h; :l; $ { x; p; b }; n; /^ *\\/ { H; bl }; x; p
+        }' "$lnl7/ftplugin/nix.vim" >> ftplugin/nix.vim
+      '';
+
+      installPhase = ''
+        cp -Rd . "$out"
+      '';
+    };
+
+    urwebAddon = fetchFromGitHub {
+      owner = "MarcWeber";
+      repo = "vim-addon-urweb";
+      rev = "49ea3960a9924a5dd7ff70956d1a7c0479a55773";
+      sha256 = "090ww8nxqsabrwf4r8g7a93kawnp6zwpsx65yxpacwwwlbc73m7s";
+    };
+
+    indentHaskell = fetchVimScript {
+      name = "haskell";
+      srcId = 7407;
+      sha256 = "1lj44jkyihmcnj2kcfckhqzr9gfipda9frbzicix2wrc5728kjsv";
+      type = "indent";
+    };
+
+    fishSyntax = fetchVimScript {
+      name = "fish";
+      srcId = 20242;
+      sha256 = "12gfmyxxf84f19bp8xfmkb9phbfkifn89sjgi8hnv6dn0a5y1zpj";
+      type = "syntax";
+    };
+
+    elmVim = fetchFromGitHub {
+      owner = "lambdatoast";
+      repo = "elm.vim";
+      rev = "ad556c97e26072b065825852ceead0fe6a1f7d7c";
+      sha256 = "19k6b6m5ngm5qn2f3p13hzjyvha53fpdgq691z8n0lwfn8831b21";
+    };
+
+    flake8 = fetchFromGitHub {
+      owner = "nvie";
+      repo = "vim-flake8";
+      rev = "293613dbe731a2875ce93739e7b64ee504d8bbab";
+      sha256 = "0xmqmbh66g44vhx9769mzs820k6ksbpfnsfvivmbhzlps2hjqpqg";
+    };
+
+    vader = fetchFromGitHub {
+      owner = "junegunn";
+      repo = "vader.vim";
+      rev = "ad2c752435baba9e7544d0046f0277c3573439bd";
+      sha256 = "0yvnah4lxk5w5qidc3y5nvl6lpi8rcv26907b3w7vjskqc935b8f";
+    };
+
+    multipleCursors = fetchFromGitHub {
+      owner = "terryma";
+      repo = "vim-multiple-cursors";
+      rev = "3afc475cc64479a406ce73d3333df1f67db3c73f";
+      sha256 = "04dijb4hgidypppphcy83bacmfrd9ikyjc761hqq6bl4kc49f5kc";
+    };
+
+    csv = fetchFromGitHub {
+      owner = "chrisbra";
+      repo = "csv.vim";
+      rev = "443fa8bd2a1a017b26cc421a9494e1a1e33f4acf";
+      sha256 = "1pbgl9f00kqxr2dpxmxg9jnk5q41sxzgan7hn16hc2b4as3zbihd";
+      extraPostFetch = ''
+        # Use sane (non-UTF8) settings for separators
+        sed -i -e 's/(&enc *[=~#]\+ *.utf-8. *?[^:]*: *\([^)]*\))/\1/g' \
+          "$out/ftplugin/csv.vim" "$out/syntax/csv.vim"
+      '';
+    };
+
+    sleuth = fetchFromGitHub {
+      owner = "tpope";
+      repo = "vim-sleuth";
+      rev = "dfe0a33253c61dd8fac455baea4ec492e6cf0fe3";
+      sha256 = "0576k4l2wbzy9frvv268vdix4k6iz9pw6n6626ifvg8hk6gbc5g9";
+    };
+
+    ats = fetchFromGitHub {
+      owner = "alex-ren";
+      repo = "org.ats-lang.toolats";
+      rev = "e0c5499dfa5c65b4aa3bf031247c768f826f3de8";
+      sha256 = "1wf8pr4pj660bxq00l9fhr07qm7mpy1jglmsyxzi9qq9pgb2avzy";
+      extraPostFetch = ''
+        mv -t "$out" "$out/org.ats-lang.toolats.vim/ftdetect" \
+                     "$out/org.ats-lang.toolats.vim/syntax"
+        rm -rf "$out/org.ats-lang.toolats.vim"
+      '';
+    };
+
+    ledger = fetchFromGitHub {
+      owner = "ledger";
+      repo = "vim-ledger";
+      rev = "6eb3bb21aa979cc295d0480b2179938c12b33d0d";
+      sha256 = "0rbwyaanvl2bqk8xm4kq8fkv8y92lpf9xx5n8gw54iij7xxhnj01";
+    };
+
+    vue = fetchFromGitHub {
+      owner = "posva";
+      repo = "vim-vue";
+      rev = "e531e1d24f24385a5f4d2f1ba36d972a57ec52d9";
+      sha256 = "1vi4i9ybwg1l1xmarsdhzd08py4w0yfg4xswbz3qrvihk8nhg1km";
+    };
+
+    meson = stdenv.mkDerivation {
+      name = "meson-vim-${meson.version}";
+      inherit (meson) src;
+      phases = [ "unpackPhase" "patchPhase" "installPhase" ];
+      postPatch = ''
+        sed -i -e '/^ *echom \+getline/d' \
+          data/syntax-highlighting/vim/indent/meson.vim
+      '';
+      installPhase = "cp -r data/syntax-highlighting/vim \"$out\"";
+    };
+
+    jinja2 = stdenv.mkDerivation {
+      name = "jinja2-vim-${python3Packages.jinja2.version}";
+      inherit (python3Packages.jinja2) src;
+      phases = [ "unpackPhase" "installPhase" ];
+      installPhase = ''
+        install -vD -m 0644 ext/Vim/jinja.vim "$out/syntax/jinja.vim"
+      '';
+    };
+
+    xdebug = fetchurl {
+      name = "vim-xt-syntax";
+      url = "https://raw.githubusercontent.com/xdebug/xdebug/"
+          + "ce4f6bc7ae04ae542960af6c1b8975888e9c3e5e/contrib/xt.vim";
+      sha256 = "05a3nry310s2w1h2q7w6yw2wick81jrnrs43x9vk0k7dqyavhvhi";
+      downloadToTemp = true;
+      recursiveHash = true;
+      postFetch = ''
+        install -vD -m 0644 "$downloadedFile" "$out/syntax/xt.vim"
+      '';
+    };
+
+    purescript = fetchFromGitHub {
+      owner = "purescript-contrib";
+      repo = "purescript-vim";
+      rev = "67ca4dc4a0291e5d8c8da48bffc0f3d2c9739e7f";
+      sha256 = "1insh39hzbynr6qxb215qxhpifl5m8i5i0d09a3b6v679i7s11i8";
+    };
+  };
+
+  generic = ''
+    " boolean
+    set nocompatible
+    set showcmd
+    set showmatch
+    set ignorecase
+    set smartcase
+    set incsearch
+    set modeline
+    set smarttab
+    set expandtab
+    set smartindent
+    set ruler
+
+    " non-boolean
+    set tabstop=4
+    set softtabstop=4
+    set shiftwidth=4
+    set textwidth=79
+    set termencoding=ascii
+    set backspace=indent,eol,start
+    set background=dark
+    set mouse=
+    set history=500
+  '';
+
+  plugin = ''
+    " erlang
+    let erlang_folding = 0
+    let erlang_highlight_bif = 1
+    let erlang_force_use_vimerl_indent = 1
+
+    " python
+    let python_highlight_numbers = 1
+    let python_highlight_builtins = 1
+    let python_highlight_exceptions = 1
+    let g:flake8_cmd = '${python3Packages.flake8}/bin/flake8'
+
+    " ledger
+    let g:ledger_bin = '${ledger}/bin/ledger'
+    let g:ledger_date_format = '%Y-%m-%d'
+    let g:ledger_maxwidth = 79
+    let g:ledger_align_at = 73
+    let g:ledger_default_commodity = 'EUR'
+    let g:ledger_commodity_before = 0
+    let g:ledger_commodity_sep = ' '
+    let g:ledger_fold_blanks = 1
+
+    " php
+    let php_noShortTags = 1
+    let php_sql_query = 1
+    let php_baselib = 1
+    let php_htmlInStrings = 1
+    let g:PHP_vintage_case_default_indent = 1
+  '';
+
+  autocmd = ''
+    " jump to last position
+    au BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") |
+                   \ exe "normal! g'\"zz" | endif
+
+    " filetype defaults
+    au BufNewFile,BufRead *.as setlocal ft=actionscript
+    au BufNewFile,BufRead *.tt setlocal ft=tt2html ts=2 sw=2 sts=2 et
+    au BufNewFile,BufRead *.xt setlocal ft=xt foldlevel=4
+    au BufNewFile,BufRead *.html setlocal ts=2 sw=2 sts=2 et
+    au FileType python setlocal textwidth=79
+    au FileType gitcommit setlocal textwidth=72
+    au FileType docbk setlocal tabstop=2 shiftwidth=2 expandtab
+
+    " Enable folding for Ledger files
+    au FileType ledger set fdm=syntax
+
+    " Do not sleuth these file types!
+    au FileType ledger let g:sleuth_automatic = 0
+    au FileType haskell let g:sleuth_automatic = 0
+    au FileType nix let g:sleuth_automatic = 0
+
+    " highlight unnecessary whitespace
+    highlight ExtraWhitespace ctermbg=red guibg=red
+    match ExtraWhitespace /\s\+$/
+    au BufWinEnter,InsertLeave * match ExtraWhitespace /\s\+$/
+    au InsertEnter * match ExtraWhitespace /\s\+\%#\@<!$/
+    " prevent colorscheme from overriding these highlights
+    au ColorScheme * highlight ExtraWhitespace ctermbg=red guibg=red
+
+    " highlight everything exceeding 79 characters (with exceptions)
+    au BufWinEnter * if index(['csv', 'strace', 'xt'], &ft) < 0
+      \ | let w:m2=matchadd('ErrorMsg', '\%>79v.\+', -1)
+      \ | endif
+
+    " flake everything that has been *detected* as python (not just by suffix)
+    au BufWritePost * if &ft ==# 'python' | call Flake8() | endif
+  '';
+
+  functions = ''
+    " ASCII art mode
+    fun! AAMode()
+      highlight clear ExtraWhitespace
+      for m in getmatches()
+        if m.group ==# 'ErrorMsg' && m.pattern ==# '\%>79v.\+'
+          call matchdelete(m.id)
+        endif
+      endfor
+    endfun
+
+    command DiffOrig vert new | set bt=nofile | r # | 0d_ | diffthis
+      \ | wincmd p | diffthis
+  '';
+
+  vimrc = writeText "vimrc" ''
+    let g:skip_defaults_vim = 1
+    ${generic}
+    ${plugin}
+
+    " has to be after the generic block and before the autocmd block
+    filetype plugin indent on
+    syntax on
+    colorscheme elflord
+
+    ${functions}
+
+    if has("autocmd")
+      ${autocmd}
+    endif
+  '';
+
+
+  installPlugin = name: plugin: let
+    mkInst = targetDir: writeScript "install-plugin-file" ''
+      #!${stdenv.shell}
+      exec install -m 0644 -vD "$1" "${targetDir}/$1"
+    '';
+
+    afterPath = "$out/share/vim/vimfiles";
+
+    findCmd = [
+      "find" "-L" "." "-mindepth" "2" "-type" "f"
+      "("  "-path" "*/after/*" "-exec" (mkInst afterPath) "{}" ";"
+      "-o" "-exec" (mkInst "$vimdir") "{}" ";"
+      ")"
+    ];
+
+  in ''
+    ( cd ${lib.escapeShellArg plugin}
+      ${lib.concatMapStringsSep " " lib.escapeShellArg findCmd}
+    )
+  '';
+
+in lib.overrideDerivation vim (o: {
+  postInstall = (o.postInstall or "") + ''
+    export vimdir="$(echo "$out/share/vim/vim"[0-9]*)"
+    ${lib.concatStrings (lib.mapAttrsToList installPlugin plugins)}
+    ln -sf "${vimrc}" "$out/share/vim/vimrc"
+  '';
+})
diff --git a/pkgs/aszlig/xournal/aspect-ratio.patch b/pkgs/aszlig/xournal/aspect-ratio.patch
new file mode 100644
index 00000000..23819904
--- /dev/null
+++ b/pkgs/aszlig/xournal/aspect-ratio.patch
@@ -0,0 +1,83 @@
+commit 4b95a904d81753b73c6ed24f65b5ff2ee84e97e2
+Author: David Barton <db9052@sourceforge.net>
+Date:   Mon Sep 4 06:27:12 2017 +0200
+
+    Preserve aspect ratio when resizing
+
+    To make the image patch even more useful, I've written this patch to
+    allow the aspect ratio to be preserved when resizing selections. (It
+    doesn't just apply to images.) Simply resize using the right mouse
+    button (button-3) rather than the left mouse button.
+
+    This patch should be applied after the enhanced image patch. (Though
+    it'd be easy enough to make the changes to the raw 0.4.5 source directly
+    since it doesn't change any of the image patch related areas.)
+
+diff --git a/src/xo-selection.c b/src/xo-selection.c
+index 7359bd8..05132b4 100644
+--- a/src/xo-selection.c
++++ b/src/xo-selection.c
+@@ -347,6 +347,12 @@ gboolean start_resizesel(GdkEvent *event)
+     ui.selection->new_x2 = ui.selection->bbox.right;
+     gnome_canvas_item_set(ui.selection->canvas_item, "dash", NULL, NULL);
+     update_cursor_for_resize(pt);
++
++    // Check whether we should preserve the aspect ratio
++    if (event->button.button == 3)
++        ui.cur_brush->tool_options |= TOOLOPT_SELECT_PRESERVE;
++    else
++        ui.cur_brush->tool_options &= ~TOOLOPT_SELECT_PRESERVE;
+     return TRUE;
+   }
+   return FALSE;
+@@ -498,6 +504,38 @@ void continue_resizesel(GdkEvent *event)
+   if (ui.selection->resizing_left) ui.selection->new_x1 = pt[0];
+   if (ui.selection->resizing_right) ui.selection->new_x2 = pt[0];
+ 
++  if (ui.cur_brush->tool_options & TOOLOPT_SELECT_PRESERVE) {
++	  double aspectratio = (ui.selection->bbox.top - ui.selection->bbox.bottom)/(ui.selection->bbox.right - ui.selection->bbox.left);
++	  double newheight = ui.selection->new_y1 - ui.selection->new_y2;
++	  double newwidth = ui.selection->new_x2 - ui.selection->new_x1;
++	  gboolean boundheight;
++
++	  // Resizing from top or bottom only
++	  if ((ui.selection->resizing_top || ui.selection->resizing_bottom) && !(ui.selection->resizing_left || ui.selection->resizing_right))
++		  boundheight = 0;
++	  // Resizing from right or left only
++	  else if (!(ui.selection->resizing_top || ui.selection->resizing_bottom) && (ui.selection->resizing_left || ui.selection->resizing_right))
++		  boundheight = 1;
++	  // Resizing from a corner
++	  else if (newheight/aspectratio > newwidth)
++		  boundheight = 0;
++	  else
++		  boundheight = 1;
++
++	  if (boundheight) {
++		  // Bound the height
++		  newheight = newwidth*aspectratio;
++		  if (ui.selection->resizing_top) ui.selection->new_y1 = ui.selection->new_y2 + newheight;
++		  else ui.selection->new_y2 = ui.selection->new_y1 - newheight;
++	  }
++	  else {
++		  // Bound the width
++		  newwidth = newheight/aspectratio;
++		  if (ui.selection->resizing_left) ui.selection->new_x1 = ui.selection->new_x2 - newwidth;
++		  else ui.selection->new_x2 = ui.selection->new_x1 + newwidth;
++	  }
++  }
++
+   gnome_canvas_item_set(ui.selection->canvas_item, 
+     "x1", ui.selection->new_x1, "x2", ui.selection->new_x2,
+     "y1", ui.selection->new_y1, "y2", ui.selection->new_y2, NULL);
+diff --git a/src/xournal.h b/src/xournal.h
+index 3599e77..e8ad4ed 100644
+--- a/src/xournal.h
++++ b/src/xournal.h
+@@ -160,6 +160,7 @@ extern guint predef_bgcolors_rgba[COLOR_MAX];
+ #define TOOLOPT_ERASER_STANDARD     0
+ #define TOOLOPT_ERASER_WHITEOUT     1
+ #define TOOLOPT_ERASER_STROKES      2
++#define TOOLOPT_SELECT_PRESERVE     1 // Preserve the aspect ratio of the selection when resizing
+ 
+ extern double predef_thickness[NUM_STROKE_TOOLS][THICKNESS_MAX];
+ 
diff --git a/pkgs/aszlig/xournal/default.nix b/pkgs/aszlig/xournal/default.nix
new file mode 100644
index 00000000..bdccc210
--- /dev/null
+++ b/pkgs/aszlig/xournal/default.nix
@@ -0,0 +1,5 @@
+{ xournal }:
+
+xournal.overrideAttrs (attrs: {
+  patches = (attrs.patches or []) ++ [ ./aspect-ratio.patch ];
+})
diff --git a/pkgs/build-support/build-sandbox/default.nix b/pkgs/build-support/build-sandbox/default.nix
new file mode 100644
index 00000000..4e5cffe9
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/default.nix
@@ -0,0 +1,107 @@
+{ stdenv, lib, pkgconfig, nix, boost, dash }:
+
+drv: { paths ? {}, ... }@attrs:
+
+let
+  # Extra paths that are required so they are created prior to bind-mounting.
+  pathsRequired    = paths.required    or [];
+  # Extra paths that are skipped if they don't exist.
+  pathsWanted      = paths.wanted      or [];
+  # Paths extracted from PATH-like environment variables, eg. LD_LIBRARY_PATH.
+  pathsRuntimeVars = paths.runtimeVars or [];
+  # Mount a dash shell in /bin/sh inside the chroot.
+  allowBinSh       = attrs.allowBinSh or false;
+  # Enable nix builds from within the sandbox.
+  # Has to write the full nix store to make the outputs accessible.
+  # TODO: get rid of nix & pkg-config if this is enabled (in the Makefile)
+  fullNixStore = attrs.fullNixStore or false;
+
+  # Create code snippets for params.c to add extra_mount() calls.
+  mkExtraMountParams = isRequired: lib.concatMapStringsSep "\n" (extra: let
+    escaped = lib.escape ["\\" "\""] extra;
+    reqBool = if isRequired then "true" else "false";
+    code = "if (!extra_mount(\"${escaped}\", ${reqBool})) return false;";
+  in "echo ${lib.escapeShellArg code} >> params.c");
+
+in stdenv.mkDerivation ({
+  name = "${drv.name}-sandboxed";
+
+  src = ./src;
+
+  inherit drv;
+
+  # writes files "sandbox-*" to the builder (see nix manual)
+  exportReferencesGraph =
+    [ "sandbox-closure" drv ] ++
+    lib.optionals allowBinSh [ "sandbox-binsh" dash ];
+
+  configurePhase = ''
+    # Reads the dependency closures and does … something? TODO: explain
+    runtimeDeps="$(sed -ne '
+      p; n; n
+
+      :cdown
+      /^0*$/b
+      :l; s/0\(X*\)$/X\1/; tl
+
+      s/^\(X*\)$/9\1/; tdone
+      ${lib.concatMapStrings (num: ''
+        s/${toString num}\(X*\)$/${toString (num - 1)}\1/; tdone
+      '') (lib.range 1 9)}
+
+      :done
+      y/X/9/
+      x; n; p; x
+      bcdown
+    ' ../sandbox-* | sort -u)"
+
+    echo '#include "setup.h"' > params.c
+    echo 'bool setup_app_paths(void) {' >> params.c
+
+    ${if fullNixStore then ''
+      # /nix/var needs to be writable for nix to work inside the sandbox
+      echo 'if (!bind_mount("/nix/var", false, true, true)) return false;' \
+        >> params.c
+      echo 'if (!bind_mount("/nix/store", true, true, true)) return false;' \
+        >> params.c
+
+    '' else ''
+      for dep in $runtimeDeps; do
+        echo 'if (!bind_mount("'"$dep"'", true, true, true)) return false;' \
+          >> params.c
+      done
+    ''}
+
+    ${mkExtraMountParams true  pathsRequired}
+    ${mkExtraMountParams false pathsWanted}
+
+    echo 'return true; }' >> params.c
+
+   ${lib.optionalString (!fullNixStore) ''
+      echo 'bool mount_runtime_path_vars(struct query_state *qs) {' >> params.c
+
+      ${lib.concatMapStringsSep "\n" (pathvar: let
+        escaped = lib.escapeShellArg (lib.escape ["\\" "\""] pathvar);
+        fun = "mount_from_path_var";
+        result = "echo 'if (!${fun}(qs, \"'${escaped}'\")) return false;'";
+      in "${result} >> params.c") pathsRuntimeVars}
+
+      echo 'return true; }' >> params.c
+    ''}
+  '';
+
+  postInstall = ''
+    for df in "$drv/share/applications/"*.desktop; do
+      mkdir -p "$out/share/applications"
+      sed -e 's!'"$drv"'/bin!'"$out"'/bin!g' "$df" \
+        > "$out/share/applications/$(basename "$df")"
+    done
+  '';
+
+  nativeBuildInputs = [ pkgconfig ];
+  buildInputs = [ boost nix ];
+  makeFlags = [ "BINDIR=${drv}/bin" ]
+           ++ lib.optional allowBinSh "BINSH_EXECUTABLE=${dash}/bin/dash"
+           ++ lib.optional fullNixStore "FULL_NIX_STORE=1";
+
+} // removeAttrs attrs [ "paths" "allowBinSh" ])
diff --git a/pkgs/build-support/build-sandbox/src/Makefile b/pkgs/build-support/build-sandbox/src/Makefile
new file mode 100644
index 00000000..8e1218f6
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/Makefile
@@ -0,0 +1,32 @@
+BINARIES = $(wildcard $(BINDIR)/*)
+WRAPPERS = $(subst $(BINDIR),$(out)/bin,$(BINARIES))
+
+OBJECTS = path-cache.o params.o setup.o
+CFLAGS = -g -Wall -std=gnu11 -DFS_ROOT_DIR=\"$(out)\"
+CXXFLAGS = -g -Wall -std=c++14 `pkg-config --cflags nix-main`
+LDFLAGS = -Wl,--copy-dt-needed-entries `pkg-config --libs nix-main`
+
+ifdef FULL_NIX_STORE
+CFLAGS += -DFULL_NIX_STORE
+else
+OBJECTS += nix-query.o
+NIX_VERSION = `pkg-config --modversion nix-main | \
+               sed -e 's/^\([0-9]\+\)\.\([0-9][0-9]\).*/\1\2/' \
+                   -e 's/^\([0-9]\+\)\.\([0-9]\).*/\10\2/'`
+CXXFLAGS += -DNIX_VERSION=$(NIX_VERSION)
+endif
+
+ifdef BINSH_EXECUTABLE
+CFLAGS += -DBINSH_EXECUTABLE=\"$(BINSH_EXECUTABLE)\"
+endif
+
+all: $(OBJECTS)
+
+$(out)/bin/%: CFLAGS += -DWRAPPED_PROGNAME=\"$(@F)\"
+$(out)/bin/%: CFLAGS += -DWRAPPED_PATH=\"$(BINDIR)/$(@F)\"
+$(out)/bin/%: $(OBJECTS)
+	mkdir -p $(out)/bin
+	$(CC) -o $@ $(CFLAGS) $(LDFLAGS) $? sandbox.c
+
+.PHONY: install
+install: $(WRAPPERS)
diff --git a/pkgs/build-support/build-sandbox/src/nix-query.cc b/pkgs/build-support/build-sandbox/src/nix-query.cc
new file mode 100644
index 00000000..71208693
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/nix-query.cc
@@ -0,0 +1,118 @@
+#include <iostream>
+
+#if NIX_VERSION >= 112
+#include <nix/config.h>
+#endif
+#include <nix/util.hh>
+#include <nix/local-store.hh>
+#include <nix/store-api.hh>
+
+#if NIX_VERSION < 112
+#include <nix/misc.hh>
+#include <nix/globals.hh>
+#endif
+
+using namespace nix;
+
+struct query_state {
+#if NIX_VERSION >= 112
+    std::shared_ptr<Store> store;
+#else
+    std::shared_ptr<StoreAPI> store;
+#endif
+    PathSet paths;
+    PathSet::iterator iter;
+};
+
+static Path get_store_path(query_state *qs, Path path)
+{
+    Path canonicalized = canonPath(path, true);
+#if NIX_VERSION >= 112
+    return qs->store->toStorePath(canonicalized);
+#else
+    return toStorePath(canonicalized);
+#endif
+}
+
+static Path get_ancestor(query_state *qs, Path path)
+{
+    size_t pos = 0;
+    std::string tmp;
+
+    while (pos != std::string::npos) {
+        if ((pos = path.find('/', pos + 1)) != std::string::npos) {
+            Path current = path.substr(0, pos);
+
+            if (!isLink(current))
+                continue;
+
+            try {
+                current = get_store_path(qs, current);
+            } catch (...) {
+                continue;
+            }
+
+            return current;
+        }
+    }
+
+    return get_store_path(qs, path);
+}
+
+extern "C" {
+    struct query_state *new_query(void)
+    {
+        query_state *initial = new query_state();
+#if NIX_VERSION >= 112
+        initial->store = openStore();
+#else
+        settings.processEnvironment();
+        settings.loadConfFile();
+        initial->store = openStore(false);
+#endif
+        return initial;
+    }
+
+    void free_query(query_state *qs)
+    {
+        delete qs;
+    }
+
+    bool query_requisites(query_state *qs, const char *path)
+    {
+        Path query(path);
+
+        try {
+            query = get_ancestor(qs, query);
+
+#if NIX_VERSION >= 112
+            qs->store->computeFSClosure(
+                qs->store->followLinksToStorePath(query),
+                qs->paths, false, true
+            );
+#else
+            computeFSClosure(
+                *qs->store, followLinksToStorePath(query),
+                qs->paths, false, true
+            );
+#endif
+        } catch (Error &e) {
+            std::cerr << "Error while querying requisites for "
+                      << query << ": " << e.what()
+                      << std::endl;
+            return false;
+        }
+
+        qs->iter = qs->paths.begin();
+
+        return true;
+    }
+
+    const char *next_query_result(query_state *qs)
+    {
+        if (qs->iter == qs->paths.end())
+            return NULL;
+
+        return (qs->iter++)->c_str();
+    }
+}
diff --git a/pkgs/build-support/build-sandbox/src/nix-query.h b/pkgs/build-support/build-sandbox/src/nix-query.h
new file mode 100644
index 00000000..3eef7c4a
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/nix-query.h
@@ -0,0 +1,6 @@
+struct query_state;
+
+struct query_state *new_query(void);
+void free_query(struct query_state *qs);
+bool query_requisites(struct query_state *qs, const char *path);
+const char *next_query_result(struct query_state *qs);
diff --git a/pkgs/build-support/build-sandbox/src/params.h b/pkgs/build-support/build-sandbox/src/params.h
new file mode 100644
index 00000000..ecfa7295
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/params.h
@@ -0,0 +1,10 @@
+#ifndef _PARAMS_H
+#define _PARAMS_H
+
+#include <stdbool.h>
+#include "nix-query.h"
+
+bool setup_app_paths(void);
+bool mount_runtime_path_vars(struct query_state *qs);
+
+#endif
diff --git a/pkgs/build-support/build-sandbox/src/path-cache.cc b/pkgs/build-support/build-sandbox/src/path-cache.cc
new file mode 100644
index 00000000..5bfa43a7
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/path-cache.cc
@@ -0,0 +1,21 @@
+#include <set>
+#include <string>
+
+typedef std::set<std::string> *path_cache;
+
+extern "C" {
+    path_cache new_path_cache(void)
+    {
+        return new std::set<std::string>();
+    }
+
+    void free_path_cache(path_cache pc)
+    {
+        delete pc;
+    }
+
+    bool cache_path(path_cache pc, const char *path)
+    {
+        return pc->insert(std::string(path)).second;
+    }
+}
diff --git a/pkgs/build-support/build-sandbox/src/path-cache.h b/pkgs/build-support/build-sandbox/src/path-cache.h
new file mode 100644
index 00000000..368f8d17
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/path-cache.h
@@ -0,0 +1,10 @@
+#ifndef _PATH_CACHE_H
+#define _PATH_CACHE_H
+
+typedef void *path_cache;
+
+path_cache new_path_cache(void);
+void free_path_cache(path_cache pc);
+bool cache_path(path_cache pc, const char *path);
+
+#endif
diff --git a/pkgs/build-support/build-sandbox/src/sandbox.c b/pkgs/build-support/build-sandbox/src/sandbox.c
new file mode 100644
index 00000000..e2aa47cf
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/sandbox.c
@@ -0,0 +1,21 @@
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "setup.h"
+
+int main(int argc, char **argv)
+{
+    if (!setup_sandbox())
+        return 1;
+
+    argv[0] = WRAPPED_PROGNAME;
+    if (execv(WRAPPED_PATH, argv) == -1) {
+        fprintf(stderr, "exec %s: %s\n", WRAPPED_PATH, strerror(errno));
+        return 1;
+    }
+
+    // Should never be reached.
+    return 1;
+}
diff --git a/pkgs/build-support/build-sandbox/src/setup.c b/pkgs/build-support/build-sandbox/src/setup.c
new file mode 100644
index 00000000..98205710
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/setup.c
@@ -0,0 +1,907 @@
+#define _GNU_SOURCE
+
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "params.h"
+#include "path-cache.h"
+#ifndef FULL_NIX_STORE
+#include "nix-query.h"
+#endif
+
+static path_cache cached_paths = NULL;
+
+static bool write_proc(int proc_pid_fd, const char *fname, const char *buf,
+                       size_t buflen, bool ignore_errors)
+{
+    int fd;
+
+    if ((fd = openat(proc_pid_fd, fname, O_WRONLY)) == -1) {
+        fprintf(stderr, "open %s: %s\n", fname, strerror(errno));
+        return false;
+    }
+
+    if (write(fd, buf, buflen) == -1) {
+        if (!ignore_errors)
+            fprintf(stderr, "write %s: %s\n", fname, strerror(errno));
+        close(fd);
+        return ignore_errors;
+    }
+
+    close(fd);
+    return true;
+}
+
+#define WRITE_IDMAP(file, value) \
+    buflen = snprintf(buf, 100, "%1$lu %1$lu 1", (unsigned long)value); \
+    if (buflen >= 100) { \
+        fputs("Unable to write buffer for " file ".\n", stderr); \
+        close(proc_pid_fd); \
+        return false; \
+    } else if (buflen < 0) { \
+        perror("snprintf " file " buffer"); \
+        close(proc_pid_fd); \
+        return false; \
+    } \
+    if (!write_proc(proc_pid_fd, file, buf, buflen, false)) { \
+        close(proc_pid_fd); \
+        return false; \
+    }
+
+bool write_maps(pid_t parent_pid)
+{
+    int proc_pid_fd;
+    size_t buflen;
+    char buf[100];
+
+    buflen = snprintf(buf, 100, "/proc/%lu", (unsigned long)parent_pid);
+    if (buflen >= 100) {
+        fputs("Unable to write buffer for child pid proc path.\n", stderr);
+        return false;
+    } else if (buflen < 0) {
+        perror("snprintf child pid proc path");
+        return false;
+    }
+
+    if ((proc_pid_fd = open(buf, O_RDONLY | O_DIRECTORY)) == -1) {
+        fprintf(stderr, "open %s: %s\n", buf, strerror(errno));
+        return false;
+    }
+
+    WRITE_IDMAP("uid_map", geteuid());
+
+    // Kernels prior to Linux 3.19 which do not impose setgroups()
+    // restrictions won't have this file, so ignore failure.
+    write_proc(proc_pid_fd, "setgroups", "deny", 4, true);
+
+    WRITE_IDMAP("gid_map", getegid());
+
+    return true;
+}
+
+static bool makedirs(const char *path, bool do_cache)
+{
+    char *tmp, *segment;
+
+    if (*path != '/') {
+        fprintf(stderr, "fatal: Path '%s' is not absolute.\n", path);
+        return false;
+    }
+
+    if ((tmp = strdup(path)) == NULL) {
+        fprintf(stderr, "strdup of %s: %s\n", path, strerror(errno));
+        return false;
+    }
+
+    segment = dirname(tmp);
+
+    if (!(segment[0] == '/' && segment[1] == '\0')) {
+        if (!makedirs(segment, do_cache)) {
+            free(tmp);
+            return false;
+        }
+    }
+
+    if (!do_cache || cache_path(cached_paths, path))
+        (void)mkdir(path, 0755);
+    free(tmp);
+    return true;
+}
+
+char *get_mount_target(const char *path)
+{
+    size_t pathlen = strlen(path), rootdir_len = strlen(FS_ROOT_DIR);
+    char *target;
+
+    if ((target = malloc(rootdir_len + pathlen + 1)) == NULL) {
+        perror("malloc mount target");
+        return NULL;
+    }
+
+    memcpy(target, FS_ROOT_DIR, rootdir_len);
+    memcpy(target + rootdir_len, path, pathlen + 1);
+    return target;
+}
+
+static bool is_regular_file(const char *path)
+{
+    struct stat st;
+    stat(path, &st);
+    return S_ISREG(st.st_mode);
+}
+
+static bool bind_file(const char *path)
+{
+    char *target, *tmp;
+
+    if (access(path, R_OK) == -1)
+        // Skip missing mount source
+        return true;
+
+    if ((target = get_mount_target(path)) == NULL)
+        return false;
+
+    if ((tmp = strdup(target)) == NULL) {
+        perror("strdup bind file target path");
+        free(target);
+        return false;
+    }
+
+    if (!makedirs(dirname(tmp), true)) {
+        free(target);
+        free(tmp);
+        return false;
+    }
+
+    free(tmp);
+
+    if (!cache_path(cached_paths, path)) {
+        free(target);
+        return true;
+    }
+
+    if (creat(target, 0666) == -1) {
+        fprintf(stderr, "unable to create %s: %s\n", target, strerror(errno));
+        free(target);
+        return false;
+    }
+
+    if (mount(path, target, "", MS_BIND, NULL) == -1) {
+        fprintf(stderr, "mount file %s to %s: %s\n",
+                path, target, strerror(errno));
+        free(target);
+        return false;
+    }
+
+    free(target);
+    return true;
+}
+
+static bool makelinks(const char *from, const char *to)
+{
+    char linktarget[PATH_MAX];
+    char *target, *tmp;
+    ssize_t linksize;
+    bool result;
+
+    if (strcmp(from, to) == 0)
+        return true;
+
+    if ((linksize = readlink(from, linktarget, PATH_MAX)) == -1) {
+        if (errno == EINVAL)
+            // Not a symbolic link
+            return true;
+
+        fprintf(stderr, "reading link %s: %s\n", from, strerror(errno));
+        return false;
+    }
+
+    linktarget[linksize] = '\0';
+
+    if ((target = get_mount_target(from)) == NULL)
+        return false;
+
+    if ((tmp = strdup(target)) == NULL) {
+        fprintf(stderr, "strdup of %s: %s\n", target, strerror(errno));
+        free(target);
+        return false;
+    }
+
+    if (!makedirs(dirname(tmp), true)) {
+        free(target);
+        free(tmp);
+        return false;
+    }
+
+    free(tmp);
+
+    if (cache_path(cached_paths, target)) {
+        if (symlink(linktarget, target) == -1) {
+            if (errno == EEXIST)
+                goto recurse;
+
+            fprintf(stderr, "creating symlink from %s to %s: %s\n",
+                    target, linktarget, strerror(errno));
+            free(target);
+            return false;
+        }
+    }
+
+recurse:
+    result = makelinks(linktarget, to);
+    free(target);
+    return result;
+}
+
+bool bind_mount(const char *path, bool rdonly, bool restricted, bool resolve)
+{
+    int base_mflags = MS_BIND | MS_REC, mflags = 0;
+    const char *msrc;
+    char src[PATH_MAX], *target;
+
+    if (rdonly)
+        mflags |= MS_RDONLY;
+
+    if (restricted)
+        mflags |= MS_NOSUID | MS_NODEV;
+
+    if (resolve ? realpath(path, src) == NULL : access(path, F_OK) == -1)
+        // Skip missing mount source
+        return true;
+
+    msrc = resolve ? src : path;
+
+    if (is_regular_file(msrc))
+        return bind_file(msrc);
+
+    if ((target = get_mount_target(msrc)) == NULL)
+        return false;
+
+    if (resolve) {
+        if (!makelinks(path, src)) {
+            free(target);
+            return false;
+        }
+    }
+
+    if (!makedirs(target, false)) {
+        free(target);
+        return false;
+    }
+
+    if (!cache_path(cached_paths, msrc)) {
+        free(target);
+        return true;
+    }
+
+    if (mount(msrc, target, "", base_mflags, NULL) == -1) {
+        fprintf(stderr, "mount %s to %s: %s\n", msrc, target, strerror(errno));
+        free(target);
+        return false;
+    }
+
+    if (mflags != 0) {
+        mflags |= base_mflags | MS_REMOUNT;
+        if (mount("none", target, "", mflags, NULL) == -1) {
+            fprintf(stderr, "remount %s: %s\n", target, strerror(errno));
+            free(target);
+            return false;
+        }
+    }
+
+    free(target);
+    return true;
+}
+
+struct envar_offset {
+    int start;
+    int length;
+    int var_start;
+    int var_length;
+    struct envar_offset *next;
+};
+
+static struct envar_offset *alloc_offset(void)
+{
+    struct envar_offset *new_offset;
+    new_offset = malloc(sizeof(struct envar_offset));
+
+    if (new_offset == NULL) {
+        perror("malloc envar_offset");
+        return NULL;
+    }
+
+    new_offset->next = NULL;
+    return new_offset;
+}
+
+static struct envar_offset *push_offset(struct envar_offset *current,
+                                        struct envar_offset **base)
+{
+    if (current == NULL) {
+        if ((current = alloc_offset()) != NULL)
+            *base = current;
+        return current;
+    }
+
+    return current->next = alloc_offset();
+}
+
+static void free_offsets(struct envar_offset *base)
+{
+    struct envar_offset *next;
+    if (base == NULL)
+        return;
+    next = base->next;
+    free(base);
+    if (next != NULL)
+        free_offsets(next);
+}
+
+#define MK_XDG_EXPAND(varname, fallback) \
+    if (strcmp(xdg_var, varname) == 0) { \
+        result = malloc(homelen + sizeof fallback); \
+        if (result == NULL) { \
+            perror("malloc " varname); \
+            return NULL; \
+        } \
+        memcpy(result, home, homelen); \
+        memcpy(result + homelen, fallback, sizeof fallback); \
+        return result; \
+    }
+
+static char *expand_xdg_fallback(const char *xdg_var)
+{
+    static char *home = NULL;
+    static size_t homelen;
+    char *result;
+
+    if (home == NULL) {
+        if ((home = getenv("HOME")) == NULL) {
+            fputs("Unable find $HOME.\n", stderr);
+            return NULL;
+        }
+        homelen = strlen(home);
+    }
+
+    MK_XDG_EXPAND("XDG_DATA_HOME", "/.local/share");
+    MK_XDG_EXPAND("XDG_CONFIG_HOME", "/.config");
+    MK_XDG_EXPAND("XDG_CACHE_HOME", "/.cache");
+
+    return NULL;
+}
+
+static char *get_offset_var(struct envar_offset *offset, const char *haystack)
+{
+    char *tmp, *result;
+
+    tmp = strndup(haystack + offset->var_start, offset->var_length);
+
+    if (tmp == NULL) {
+        perror("strndup");
+        return NULL;
+    }
+
+    result = getenv(tmp);
+    if (result == NULL) {
+        if ((result = expand_xdg_fallback(tmp)) == NULL) {
+            fprintf(stderr, "Unable find variable %s in %s\n", tmp, haystack);
+            free(tmp);
+            return NULL;
+        }
+        free(tmp);
+        return result;
+    }
+    free(tmp);
+    return strdup(result);
+}
+
+static char *replace_env_offset_free(const char *path,
+                                     struct envar_offset *offset)
+{
+    struct envar_offset *tmp_offset;
+    size_t buflen, pathlen, varlen, tmplen;
+    int inpos = 0, outpos = 0;
+    char *buf, *curvar;
+
+    buflen = pathlen = strlen(path);
+
+    if ((buf = malloc(buflen + 1)) == NULL) {
+        perror("malloc replace_env buffer");
+        return NULL;
+    }
+
+    while (offset != NULL) {
+        if ((curvar = get_offset_var(offset, path)) == NULL) {
+            free(buf);
+            free_offsets(offset);
+            return NULL;
+        }
+
+        varlen = strlen(curvar);
+        tmplen = varlen + (buflen - offset->length);
+
+        if (tmplen > buflen) {
+            if ((buf = realloc(buf, (buflen = tmplen) + 1)) == NULL) {
+                perror("realloc replace_env buffer");
+                free(buf);
+                free(curvar);
+                free_offsets(offset);
+                return NULL;
+            }
+        }
+
+        memcpy(buf + outpos, path + inpos, offset->start - inpos);
+        outpos += offset->start - inpos;
+        inpos = offset->start;
+
+        memcpy(buf + outpos, curvar, varlen);
+        outpos += varlen;
+        inpos += offset->length;
+
+        free(curvar);
+
+        tmp_offset = offset;
+        offset = offset->next;
+        free(tmp_offset);
+    }
+
+    memcpy(buf + outpos, path + inpos, pathlen - inpos);
+    *(buf + outpos + (pathlen - inpos)) = '\0';
+
+    return buf;
+}
+
+static char *replace_env(const char *path)
+{
+    int i = 0, start = 0, var_start = 0;
+    size_t pathlen;
+    bool in_var = false, curly = false;
+    struct envar_offset *base = NULL, *offset = NULL;
+
+    pathlen = strlen(path);
+
+    while (i < pathlen) {
+        if (path[i] == '$' && !curly && !in_var) {
+            if (i + 1 >= pathlen)
+                break;
+
+            start = i;
+
+            if (path[i + 1] == '{') {
+                curly = true;
+                var_start = i + 2;
+                ++i;
+            } else {
+                in_var = true;
+                var_start = i + 1;
+            }
+        } else if (in_var) {
+            if (!(path[i] >= 'a' && path[i] <= 'z') &&
+                !(path[i] >= 'A' && path[i] <= 'Z') &&
+                !(path[i] >= '0' && path[i] <= '9') &&
+                path[i] != '_'
+            ) {
+                in_var = false;
+
+                if ((offset = push_offset(offset, &base)) == NULL) {
+                    free_offsets(base);
+                    return NULL;
+                }
+
+                offset->start = start;
+                offset->length = i - start;
+                offset->var_start = var_start;
+                offset->var_length = i - var_start;
+                continue;
+            }
+        } else if (curly) {
+            if (path[i] == '}') {
+                curly = false;
+
+                if ((offset = push_offset(offset, &base)) == NULL) {
+                    free_offsets(base);
+                    return NULL;
+                }
+
+                offset->start = start;
+                offset->length = (i + 1) - offset->start;
+                offset->var_start = var_start;
+                offset->var_length = i - offset->var_start;
+            }
+        }
+
+        ++i;
+    }
+
+    if (in_var) {
+        if ((offset = push_offset(offset, &base)) == NULL) {
+            free_offsets(base);
+            return NULL;
+        }
+
+        offset->start = start;
+        offset->length = i - start;
+        offset->var_start = var_start;
+        offset->var_length = i - var_start;
+    }
+
+    return replace_env_offset_free(path, base);
+}
+
+bool extra_mount(const char *path, bool is_required)
+{
+    char *expanded;
+
+    if ((expanded = replace_env(path)) == NULL)
+        return false;
+
+    if (is_required && !makedirs(expanded, false))
+        return false;
+
+    if (!bind_mount(expanded, false, true, true)) {
+        free(expanded);
+        return false;
+    }
+
+    free(expanded);
+    return true;
+}
+
+static bool setup_xauthority(void)
+{
+    char *xauth, *home;
+    bool result;
+    size_t homelen;
+
+    if ((xauth = getenv("XAUTHORITY")) != NULL)
+        return bind_file(xauth);
+
+    if ((home = getenv("HOME")) == NULL) {
+        fputs("Unable find $HOME.\n", stderr);
+        return false;
+    }
+
+    homelen = strlen(home);
+
+    if ((xauth = malloc(homelen + 13)) == NULL) {
+        perror("malloc xauth file path");
+        return false;
+    }
+
+    memcpy(xauth, home, homelen);
+    memcpy(xauth + homelen, "/.Xauthority", 13);
+
+    result = bind_file(xauth);
+    free(xauth);
+    return result;
+}
+
+#ifdef BINSH_EXECUTABLE
+static bool setup_binsh(const char *executable)
+{
+    if (!makedirs(FS_ROOT_DIR "/bin", false))
+        return false;
+
+    if (symlink(executable, FS_ROOT_DIR "/bin/sh") == -1) {
+        fprintf(stderr, "creating symlink from %s to %s: %s\n",
+                executable, FS_ROOT_DIR "/bin/sh", strerror(errno));
+        return false;
+    }
+    return true;
+}
+#endif
+
+#ifndef FULL_NIX_STORE
+static bool is_dir(const char *path)
+{
+    struct stat sb;
+    if (stat(path, &sb) == -1) {
+        fprintf(stderr, "stat %s: %s\n", path, strerror(errno));
+        // Default to directory for mounting
+        return true;
+    }
+    return S_ISDIR(sb.st_mode);
+}
+
+static bool mount_requisites(struct query_state *qs, const char *path)
+{
+    const char *requisite;
+
+    if (!query_requisites(qs, path)) {
+        fprintf(stderr, "Unable to get requisites for %s.\n", path);
+        return false;
+    }
+
+    while ((requisite = next_query_result(qs)) != NULL) {
+        if (is_dir(requisite)) {
+            if (!bind_mount(requisite, true, true, false))
+                return false;
+        } else {
+            if (!bind_file(requisite))
+                return false;
+        }
+    }
+
+    return true;
+}
+
+bool mount_from_path_var(struct query_state *qs, const char *name)
+{
+    char *buf, *ptr, *value = getenv(name);
+
+    if (value == NULL)
+        return true;
+
+    if ((buf = strdup(value)) == NULL) {
+        fprintf(stderr, "strdup %s: %s\n", value, strerror(errno));
+        return false;
+    }
+
+    ptr = strtok(buf, ":");
+
+    while (ptr != NULL) {
+        if (!mount_requisites(qs, ptr)) {
+            free(buf);
+            return false;
+        }
+        ptr = strtok(NULL, ":");
+    }
+
+    free(buf);
+    return true;
+}
+
+/* `/etc/static` is a special symlink on NixOS, pointing to a storepath
+   of configs that have to be available at runtime for some programs
+   to function. So we need to mount the closure of that storepath. */
+static bool setup_static_etc(struct query_state *qs)
+{
+    char dest[PATH_MAX];
+    ssize_t destlen;
+
+    if ((destlen = readlink("/etc/static", dest, PATH_MAX)) == -1)
+        return true;
+
+    if (destlen >= PATH_MAX) {
+        fputs("readlink of /etc/static larger than PATH_MAX.\n", stderr);
+        return false;
+    }
+
+    dest[destlen] = '\0';
+    return mount_requisites(qs, dest);
+}
+
+/* Bind-mount all necessary nix store paths. */
+static bool setup_runtime_paths(void)
+{
+    struct query_state *qs;
+
+    if ((qs = new_query()) == NULL) {
+        fputs("Unable to allocate Nix query state.\n", stderr);
+        return false;
+    }
+
+    if (!setup_static_etc(qs)) {
+        free_query(qs);
+        return false;
+    }
+
+    if (!mount_runtime_path_vars(qs)) {
+        free_query(qs);
+        return false;
+    }
+
+    free_query(qs);
+    return true;
+}
+#endif
+
+static bool setup_runtime_debug(void)
+{
+    char *injected_files, *buf, *ptr, *equals, *target;
+
+    if ((injected_files = getenv("NIX_SANDBOX_DEBUG_INJECT_FILES")) == NULL)
+        return true;
+
+    if ((buf = strdup(injected_files)) == NULL) {
+        perror("strdup NIX_SANDBOX_DEBUG_INJECT_FILES");
+        return false;
+    }
+
+    ptr = strtok(buf, ":");
+
+    while (ptr != NULL) {
+        if ((equals = strchr(ptr, '=')) != NULL) {
+            *equals = '\0';
+
+            if ((target = get_mount_target(equals + 1)) == NULL) {
+                free(buf);
+                return false;
+            }
+
+            if (mount(ptr, target, "", MS_BIND, NULL) == -1) {
+                fprintf(stderr, "mount injected file %s to %s: %s\n",
+                        ptr, target, strerror(errno));
+                free(target);
+                free(buf);
+                return false;
+            }
+
+            free(target);
+            fprintf(stderr, "Injected file '%s' to '%s'.\n", ptr, equals + 1);
+        }
+
+        ptr = strtok(NULL, ":");
+    }
+
+    free(buf);
+    return true;
+}
+
+static bool setup_chroot(void)
+{
+    int mflags;
+
+    mflags = MS_NOEXEC | MS_NOSUID | MS_NODEV | MS_NOATIME;
+
+    if (mount("none", FS_ROOT_DIR, "tmpfs", mflags, NULL) == -1) {
+        perror("mount rootfs");
+        return false;
+    }
+
+    if (!bind_mount("/etc", true, true, false))
+        return false;
+
+    if (!bind_mount("/dev", false, false, false))
+        return false;
+
+    if (!makedirs(FS_ROOT_DIR "/proc", false))
+        return false;
+
+    if (mount("none", FS_ROOT_DIR "/proc", "proc", 0, NULL) == -1) {
+        perror("mount /proc");
+        return false;
+    }
+
+    if (!bind_mount("/sys", false, false, false))
+        return false;
+
+    if (!bind_mount("/run", false, false, false))
+        return false;
+
+    if (!bind_mount("/var/run", false, false, false))
+        return false;
+
+    if (!bind_mount("/tmp", false, true, false))
+        return false;
+
+    // We don’t need to query the nix store if we mount the full store
+#ifndef FULL_NIX_STORE
+    if (!setup_runtime_paths())
+        return false;
+#endif
+
+    if (!setup_app_paths())
+        return false;
+
+    if (!setup_xauthority())
+        return false;
+
+    if (!setup_runtime_debug())
+        return false;
+
+#ifdef BINSH_EXECUTABLE
+    if (!setup_binsh(BINSH_EXECUTABLE))
+        return false;
+#endif
+
+    if (chroot(FS_ROOT_DIR) == -1) {
+        perror("chroot");
+        return false;
+    }
+
+    if (chdir("/") == -1) {
+        perror("chdir rootfs");
+        return false;
+    }
+
+    return true;
+}
+
+bool setup_sandbox(void)
+{
+    int sync_pipe[2];
+    char sync_status = '.';
+    int child_status;
+    pid_t pid, parent_pid;
+
+    if (pipe(sync_pipe) == -1) {
+        perror("pipe");
+        return false;
+    }
+
+    parent_pid = getpid();
+
+    switch (pid = fork()) {
+        case -1:
+            perror("fork");
+            return false;
+        case 0:
+            close(sync_pipe[1]);
+            if (read(sync_pipe[0], &sync_status, 1) == -1) {
+                perror("read pipe from parent");
+                _exit(1);
+            } else if (sync_status == 'X')
+                _exit(1);
+            close(sync_pipe[0]);
+            _exit(write_maps(parent_pid) ? 0 : 1);
+        default:
+            if (unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWPID |
+                        CLONE_NEWUTS | CLONE_NEWIPC) == -1) {
+                perror("unshare");
+                if (write(sync_pipe[1], "X", 1) == -1)
+                    perror("signal child exit");
+                waitpid(pid, NULL, 0);
+                return false;
+            }
+
+            close(sync_pipe[1]);
+            waitpid(pid, &child_status, 0);
+            if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 0)
+                break;
+            return false;
+    }
+
+    if ((pid = fork()) == -1) {
+        perror("fork PID namespace");
+        return false;
+    }
+
+    /* Just wait in the parent until the child exits. We need to fork because
+     * otherwise we can't mount /proc in the right PID namespace.
+     */
+    int wstatus;
+    if (pid > 0) {
+
+        if (waitpid(pid, &wstatus, 0) == -1) {
+          fputs("sandbox: waitpid failure", stderr);
+          _exit(EXIT_FAILURE);
+        }
+        else if (WIFEXITED(wstatus)) {
+          _exit(WEXITSTATUS(wstatus));
+        }
+        else if (WIFSIGNALED(wstatus)) {
+          fprintf(stderr, "sandbox: killed by signal %d\n", WTERMSIG(wstatus));
+          _exit(EXIT_FAILURE);
+        }
+        else {
+          // WIFSTOPPED, WIFCONTINUED?
+          fputs("sandbox: wait failed", stderr);
+          _exit(EXIT_FAILURE);
+        }
+    }
+
+    cached_paths = new_path_cache();
+
+    if (!setup_chroot()) {
+        free_path_cache(cached_paths);
+        return false;
+    }
+
+    free_path_cache(cached_paths);
+    return true;
+}
diff --git a/pkgs/build-support/build-sandbox/src/setup.h b/pkgs/build-support/build-sandbox/src/setup.h
new file mode 100644
index 00000000..6d9dd0a3
--- /dev/null
+++ b/pkgs/build-support/build-sandbox/src/setup.h
@@ -0,0 +1,15 @@
+#ifndef _SETUP_H
+#define _SETUP_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include "nix-query.h"
+
+char *get_mount_target(const char *path);
+bool write_maps(pid_t parent_pid);
+bool bind_mount(const char *path, bool rdonly, bool restricted, bool resolve);
+bool extra_mount(const char *path, bool is_required);
+bool mount_from_path_var(struct query_state *qs, const char *name);
+bool setup_sandbox(void);
+
+#endif
diff --git a/pkgs/build-support/channel.nix b/pkgs/build-support/channel.nix
new file mode 100644
index 00000000..a837177f
--- /dev/null
+++ b/pkgs/build-support/channel.nix
@@ -0,0 +1,32 @@
+{ stdenv }:
+
+{ name, src, constituents ? [], meta ? {}, ... }@args:
+
+stdenv.mkDerivation ({
+  inherit name src constituents;
+  preferLocalBuild = true;
+  _hydraAggregate = true;
+
+  phases = [ "unpackPhase" "patchPhase" "installPhase" ];
+  installPhase = ''
+    mkdir -p "$out/tarballs" "$out/nix-support"
+
+    tar cJf "$out/tarballs/nixexprs.tar.xz" \
+      --owner=0 --group=0 --mtime="1970-01-01 00:00:00 UTC" \
+      --transform='s!^\.!${name}!' .
+
+    echo "channel - $out/tarballs/nixexprs.tar.xz" \
+      > "$out/nix-support/hydra-build-products"
+
+    echo $constituents > "$out/nix-support/hydra-aggregate-constituents"
+    for i in $constituents; do
+      if [ -e "$i/nix-support/failed" ]; then
+        touch "$out/nix-support/failed"
+      fi
+    done
+  '';
+
+  meta = meta // {
+    isHydraChannel = true;
+  };
+} // removeAttrs args [ "name" "channelName" "src" "constituents" "meta" ])
diff --git a/pkgs/default.nix b/pkgs/default.nix
new file mode 100644
index 00000000..070ae786
--- /dev/null
+++ b/pkgs/default.nix
@@ -0,0 +1,32 @@
+{ pkgs ? import (import ../nixpkgs-path.nix) {} }:
+
+let
+  inherit (pkgs.lib) callPackageWith;
+  callPackage = callPackageWith (pkgs // self.vuizvui);
+  callPackage_i686 = callPackageWith (pkgs.pkgsi686Linux // self.vuizvui);
+
+  callPackageScope = import ./lib/call-package-scope.nix {
+    pkgs = pkgs // self.vuizvui;
+    pkgsi686Linux = pkgs.pkgsi686Linux // self.vuizvui;
+  };
+
+  self.vuizvui = pkgs.recurseIntoAttrs {
+    mkChannel = callPackage ./build-support/channel.nix { };
+    buildSandbox = callPackage build-support/build-sandbox {};
+
+    list-gamecontrollers = callPackage ./list-gamecontrollers { };
+
+    games = import ./games {
+      pkgs = pkgs // self.vuizvui;
+      pkgsi686Linux = pkgs.pkgsi686Linux // self.vuizvui;
+      config = pkgs.config.vuizvui.games or null;
+    };
+
+    taalo-build = callPackage ./taalo-build { };
+
+    aszlig = callPackageScope ./aszlig;
+    openlab = callPackageScope ./openlab;
+    profpatsch = callPackageScope ./profpatsch;
+    sternenseemann = callPackageScope ./sternenseemann;
+  };
+in self.vuizvui
diff --git a/pkgs/games/build-support/build-game.nix b/pkgs/games/build-support/build-game.nix
new file mode 100644
index 00000000..459fca75
--- /dev/null
+++ b/pkgs/games/build-support/build-game.nix
@@ -0,0 +1,59 @@
+{ stdenv, lib, file, unzip, buildSandbox, autoPatchelfHook, gogUnpackHook
+
+, withPulseAudio ? true, libpulseaudio ? null
+, alsaLib
+}:
+
+assert withPulseAudio -> libpulseaudio != null;
+
+{ buildInputs ? []
+, nativeBuildInputs ? []
+, preUnpack ? ""
+, setSourceRoot ? ""
+, runtimeDependencies ? []
+, sandbox ? {}
+, ...
+}@attrs:
+
+buildSandbox (stdenv.mkDerivation ({
+  buildInputs = [ stdenv.cc.cc ] ++ buildInputs;
+
+  nativeBuildInputs = [
+    unzip autoPatchelfHook gogUnpackHook
+  ] ++ nativeBuildInputs;
+
+  preUnpack = preUnpack + ''
+    mkdir "$name"
+    pushd "$name" &> /dev/null
+  '';
+
+  # Try to evade tarbombs
+  setSourceRoot = ''
+    popd &> /dev/null
+  '' + lib.optionalString (setSourceRoot == "") ''
+    sourceRoot="$(find "$name" -type d -exec sh -c '
+      ndirs="$(find "$1" -mindepth 1 -maxdepth 1 -type d -printf x | wc -m)"
+      nelse="$(find "$1" -mindepth 1 -maxdepth 1 ! -type d -printf x | wc -m)"
+      ! [ "$ndirs" -eq 1 -a "$nelse" -eq 0 ]
+    ' -- {} \; -print -quit)"
+  '';
+
+  runtimeDependencies = let
+    deps = lib.singleton alsaLib
+        ++ lib.optional withPulseAudio libpulseaudio
+        ++ runtimeDependencies;
+  in map (dep: dep.lib or dep) deps;
+
+  dontStrip = true;
+  dontPatchELF = true;
+} // removeAttrs attrs [
+  "buildInputs" "nativeBuildInputs" "preUnpack" "setSourceRoot"
+  "runtimeDependencies" "sandbox"
+])) (sandbox // {
+  paths = let
+    paths = sandbox.paths or {};
+  in paths // {
+    required = paths.required or [ "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" ];
+    runtimeVars = [ "LD_LIBRARY_PATH" ] ++ paths.runtimeVars or [];
+  };
+})
diff --git a/pkgs/games/build-support/build-unity.nix b/pkgs/games/build-support/build-unity.nix
new file mode 100644
index 00000000..4169870c
--- /dev/null
+++ b/pkgs/games/build-support/build-unity.nix
@@ -0,0 +1,80 @@
+{ stdenv, lib, buildGame, makeWrapper, gtk2-x11, gdk_pixbuf, glib
+, libGL, xorg, libpulseaudio, libudev, zlib
+}:
+
+{ name, version, fullName
+, saveDir ? null
+, nativeBuildInputs ? []
+, buildInputs ? []
+, runtimeDependencies ? []
+, sandbox ? {}
+, ...
+}@attrs:
+
+let
+  arch = if stdenv.system == "x86_64-linux" then "x86_64" else "x86";
+  executable = "${fullName}.${arch}";
+  dataDir = "${fullName}_Data";
+  maybeSavedir = lib.optionalString (saveDir != null) "/${saveDir}";
+
+in buildGame ({
+  name = "${name}-${version}";
+  inherit fullName version arch executable dataDir;
+  slugName = name;
+
+  nativeBuildInputs = [ makeWrapper ] ++ nativeBuildInputs;
+
+  buildInputs = [ gtk2-x11 gdk_pixbuf glib ] ++ buildInputs;
+
+  runtimeDependencies = [
+    libGL xorg.libX11 xorg.libXcursor xorg.libXrandr libudev zlib
+  ] ++ runtimeDependencies;
+
+  sandbox = sandbox // {
+    paths = (sandbox.paths or {}) // {
+      required = (sandbox.paths.required or []) ++ [
+        "$XDG_CONFIG_HOME/unity3d${maybeSavedir}"
+      ];
+    };
+  };
+
+  installPhase = ''
+    runHook preInstall
+
+    install -vD "$executable" "$out/libexec/$slugName/$slugName"
+    ln -s "$out/share/$slugName" "$out/libexec/$slugName/Data"
+
+    mkdir -p "$out/bin"
+    makeWrapper "$out/libexec/$slugName/$slugName" "$out/bin/$slugName" \
+      --run "cd '$out/share/$slugName'"
+
+    iconpath="$out/share/$slugName/Resources/UnityPlayer.png"
+    mkdir -p "$out/share/applications"
+    cat > "$out/share/applications/$slugName.desktop" <<EOF
+    [Desktop Entry]
+    Name=$fullName
+    Type=Application
+    Version=1.1
+    Exec=$out/bin/$slugName
+    Icon=$iconpath
+    Categories=Game
+    StartupNotify=true
+    EOF
+
+    cp -vRd "$dataDir" "$out/share/$slugName"
+
+    if [ -d "$fullName.app" ]; then
+      cp -vRd -t "$out/share/$slugName" "$fullName.app"
+    fi
+
+    if [ ! -e "$iconpath" ]; then
+      echo "Desktop icon not found at $iconpath." >&2
+      exit 1
+    fi
+
+    runHook postInstall
+  '';
+} // removeAttrs attrs [
+  "name" "version" "fullName" "nativeBuildInputs" "buildInputs"
+  "runtimeDependencies" "sandbox"
+])
diff --git a/pkgs/games/build-support/default.nix b/pkgs/games/build-support/default.nix
new file mode 100644
index 00000000..ac152659
--- /dev/null
+++ b/pkgs/games/build-support/default.nix
@@ -0,0 +1,11 @@
+{ config, callPackage, callPackages, ... }:
+
+{
+  buildGame = callPackage ./build-game.nix {
+    withPulseAudio = config.pulseaudio or true;
+  };
+  buildUnity = callPackage ./build-unity.nix {};
+  monogamePatcher = callPackage ./monogame-patcher {};
+
+  inherit (callPackages ./setup-hooks {}) fixFmodHook gogUnpackHook;
+}
diff --git a/pkgs/games/build-support/monogame-patcher/default.nix b/pkgs/games/build-support/monogame-patcher/default.nix
new file mode 100644
index 00000000..74ad83bf
--- /dev/null
+++ b/pkgs/games/build-support/monogame-patcher/default.nix
@@ -0,0 +1,27 @@
+{ lib, buildDotnetPackage, fetchNuGet }:
+
+buildDotnetPackage {
+  baseName = "monogame-patcher";
+  version = "0.1.0";
+
+  src = lib.cleanSource ./src;
+
+  buildInputs = [
+    (fetchNuGet {
+      baseName = "Mono.Cecil";
+      version = "0.10-beta7";
+      sha256 = "1ngjxk3wbmdwgsbdpy9yjwgc0ii8xxa78i0h57dia2rjn0gr7bw0";
+      outputFiles = [ "lib/net40/*" ];
+    })
+
+    (fetchNuGet {
+      baseName = "CommandLineParser";
+      version = "2.2.1";
+      sha256 = "02zqp98lzjv4rpjf7jl0hvhda41dlh0dc29axaapq9glk0hbmjzg";
+      outputFiles = [ "lib/net45/*" ];
+    })
+  ];
+
+  doInstallCheck = true;
+  installCheckPhase = "$SHELL -e test.sh";
+}
diff --git a/pkgs/games/build-support/monogame-patcher/src/assembly-info.cs b/pkgs/games/build-support/monogame-patcher/src/assembly-info.cs
new file mode 100644
index 00000000..55c4c4d3
--- /dev/null
+++ b/pkgs/games/build-support/monogame-patcher/src/assembly-info.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+
+[assembly: AssemblyTitle("monogame-patcher")]
+[assembly: AssemblyVersion("0.1.0")]
+[assembly: AssemblyDescription("Patches common annoyances in Mono Assemblies")]
+[assembly: AssemblyCopyright("(c) 2018 aszlig")]
diff --git a/pkgs/games/build-support/monogame-patcher/src/options.cs b/pkgs/games/build-support/monogame-patcher/src/options.cs
new file mode 100644
index 00000000..73add8d6
--- /dev/null
+++ b/pkgs/games/build-support/monogame-patcher/src/options.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+using CommandLine;
+
+class GenericOptions
+{
+    [Option('i', "infile", Required=true, HelpText="Input file to transform.")]
+    public string inputFile { get; set; }
+    [Option('o', "outfile", HelpText="File to write transformed data to.")]
+    public string outputFile { get; set; }
+}
+
+[Verb("fix-filestreams", HelpText="Fix System.IO.FileStream constructors"
+                                 +" to open files read-only.")]
+class FixFileStreamsCmd : GenericOptions {
+    [Value(0, Required=true, MetaName = "type", HelpText = "Types to patch.")]
+    public IEnumerable<string> typesToPatch { get; set; }
+};
+
+[Verb("replace-call", HelpText="Replace calls to types.")]
+class ReplaceCallCmd : GenericOptions {
+    [Value(0, Required=true, HelpText="Method call to replace.")]
+    public string replaceMethod { get; set; }
+
+    [Value(1, Required=true, HelpText="The replacement method.")]
+    public string replacementMethod { get; set; }
+
+    [Value(2, Required=true, MetaName = "type", HelpText = "Types to patch.")]
+    public IEnumerable<string> typesToPatch { get; set; }
+
+    [Option('a', "assembly",
+            HelpText="Look up replacement from the specified assembly.")]
+    public string assemblyFile { get; set; }
+};
diff --git a/pkgs/games/build-support/monogame-patcher/src/patcher.cs b/pkgs/games/build-support/monogame-patcher/src/patcher.cs
new file mode 100644
index 00000000..50e50fe6
--- /dev/null
+++ b/pkgs/games/build-support/monogame-patcher/src/patcher.cs
@@ -0,0 +1,202 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System;
+
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
+using Mono.Cecil;
+
+using CommandLine;
+
+class Command {
+    protected string infile;
+    protected string outfile;
+    protected ModuleDefinition module;
+
+    public Command(GenericOptions options) {
+        if (options.outputFile == null)
+            this.outfile = options.inputFile;
+        else
+            this.outfile = options.outputFile;
+        this.infile = options.inputFile;
+
+        var readOnly = this.infile != this.outfile;
+        this.module = this.read_module(this.infile, readOnly);
+    }
+
+    private IEnumerable<MethodDefinition> get_methods(TypeDefinition td) {
+        return td.NestedTypes
+            .SelectMany(n => this.get_methods(n))
+            .Union(td.Methods);
+    }
+
+    protected IEnumerable<MethodDefinition> getm(IEnumerable<string> types) {
+        var needles = types
+            .Select(t => t.Split(new[] { "::" }, 2, StringSplitOptions.None));
+
+        foreach (var n in needles) {
+            var filtered = this.module.Types.Where(p => p.Name == n[0]);
+            var methods = filtered.SelectMany(t => this.get_methods(t));
+
+            var found = false;
+
+            if (n.Length == 1) {
+                foreach (var m in methods)
+                    yield return m;
+                found = true;
+            } else {
+                foreach (var m in methods) {
+                    if (m.Name == n[1]) {
+                        found = true;
+                        yield return m;
+                    }
+                }
+            }
+
+            if (!found) {
+                var thetype = string.Join("::", n);
+                throw new Exception($"Type {thetype} not found.");
+            }
+        }
+    }
+
+    protected ModuleDefinition read_module(string path, bool readOnly) {
+        var resolver = new DefaultAssemblyResolver();
+        resolver.AddSearchDirectory(Path.GetDirectoryName(path));
+
+        var rp = new ReaderParameters {
+            ReadWrite = !readOnly,
+            AssemblyResolver = resolver
+        };
+        return ModuleDefinition.ReadModule(path, rp);
+    }
+
+    protected virtual IEnumerable<TypeDefinition> get_assembly_types() {
+        return this.module.AssemblyReferences
+            .Select(a => this.module.AssemblyResolver.Resolve(a))
+            .SelectMany(r => r.MainModule.Types);
+    }
+
+    protected MethodReference find_method_ref(string fullSig) {
+        foreach (var type in this.get_assembly_types()) {
+            foreach (var ctor in type.GetConstructors()) {
+                if (ctor.ToString() != fullSig) continue;
+                return this.module.ImportReference(ctor);
+            }
+            foreach (var meth in type.GetMethods()) {
+                if (meth.ToString() != fullSig) continue;
+                return this.module.ImportReference(meth);
+            }
+        }
+
+        throw new Exception($"Method reference for {fullSig} not found.");
+    }
+
+    public void save() {
+        if (this.outfile == this.infile)
+            this.module.Write();
+        else
+            this.module.Write(this.outfile);
+    }
+}
+
+class FixFileStreams : Command {
+    private MethodReference betterFileStream;
+
+    public FixFileStreams(FixFileStreamsCmd options) : base(options) {
+        this.betterFileStream = this.find_method_ref(
+            "System.Void System.IO.FileStream::.ctor" +
+            "(System.String,System.IO.FileMode,System.IO.FileAccess)"
+        );
+
+        foreach (var toPatch in this.getm(options.typesToPatch))
+            patch_method(toPatch);
+
+        this.save();
+    }
+
+    private void patch_method(MethodDefinition md) {
+        var il = md.Body.GetILProcessor();
+
+        var fileStreams = md.Body.Instructions
+            .Where(i => i.OpCode == OpCodes.Newobj)
+            .Where(i => (i.Operand as MethodReference).DeclaringType
+                    .FullName == "System.IO.FileStream");
+
+        foreach (Instruction i in fileStreams.ToList()) {
+            var fileAccessRead = il.Create(OpCodes.Ldc_I4_1);
+            il.InsertBefore(i, fileAccessRead);
+            il.Replace(i, il.Create(OpCodes.Newobj, this.betterFileStream));
+        }
+    }
+}
+
+class ReplaceCall : Command {
+    private string search;
+    private MethodReference replace;
+    private ModuleDefinition targetModule;
+    private bool patch_done;
+
+    public ReplaceCall(ReplaceCallCmd options) : base(options) {
+        if (options.assemblyFile != null)
+            this.targetModule = this.read_module(options.assemblyFile, true);
+
+        this.search = options.replaceMethod;
+        this.replace = this.find_method_ref(options.replacementMethod);
+
+        this.patch_done = false;
+
+        foreach (var toPatch in this.getm(options.typesToPatch))
+            patch_method(toPatch);
+
+        if (!this.patch_done) {
+            var types = string.Join(", ", options.typesToPatch);
+            throw new Exception($"Unable to find {this.search} in {types}.");
+        }
+
+        this.save();
+    }
+
+    protected override IEnumerable<TypeDefinition> get_assembly_types() {
+        if (this.targetModule != null)
+            return this.targetModule.Types;
+        else
+            return base.get_assembly_types();
+    }
+
+    private void patch_method(MethodDefinition md) {
+        var il = md.Body.GetILProcessor();
+
+        var found = md.Body.Instructions
+            .Where(i => i.OpCode == OpCodes.Call ||
+                        i.OpCode == OpCodes.Callvirt)
+            .Where(i => i.Operand.ToString() == this.search);
+
+        foreach (Instruction i in found.ToList()) {
+            il.Replace(i, il.Create(OpCodes.Call, this.replace));
+            this.patch_done = true;
+        }
+    }
+}
+
+public class patcher {
+    public static int Main(string[] args) {
+        var parser = new Parser((settings) => {
+            settings.EnableDashDash = true;
+            settings.HelpWriter = Console.Error;
+
+            // XXX: When not running in a terminal the width is 0, but the
+            //      CommandLine library expects it to be greater than zero.
+            if (Console.WindowWidth == 0)
+                settings.MaximumDisplayWidth = 80;
+        });
+
+        var retval = 0;
+        parser.ParseArguments<FixFileStreamsCmd, ReplaceCallCmd>(args)
+            .WithParsed<FixFileStreamsCmd>(opts => new FixFileStreams(opts))
+            .WithParsed<ReplaceCallCmd>(opts => new ReplaceCall(opts))
+            .WithNotParsed(_ => retval = 1);
+        return retval;
+    }
+}
diff --git a/pkgs/games/build-support/monogame-patcher/src/patcher.csproj b/pkgs/games/build-support/monogame-patcher/src/patcher.csproj
new file mode 100644
index 00000000..03307287
--- /dev/null
+++ b/pkgs/games/build-support/monogame-patcher/src/patcher.csproj
@@ -0,0 +1,43 @@
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
+         DefaultTargets="Build">
+
+  <Import Project="$(MSBuildToolsPath)/Microsoft.CSharp.targets" />
+
+  <ItemGroup>
+    <Compile Include="assembly-info.cs"/>
+    <Compile Include="options.cs"/>
+    <Compile Include="patcher.cs"/>
+  </ItemGroup>
+
+  <PropertyGroup>
+    <!-- I *hate* those long lines with a passion, so let's concat them -->
+    <Blurb>, Culture=neutral, PublicKeyToken=</Blurb>
+
+    <CecilInfo>Version=0.10.0.0$(Blurb)50cebf1cceb9d05e</CecilInfo>
+    <CmdLineInfo>Version=2.2.1.0$(Blurb)de6f01bd326f8c32</CmdLineInfo>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Mono.Cecil, $(CecilInfo)">
+      <SpecificVersion>False</SpecificVersion>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="Mono.Cecil.Rocks, $(CecilInfo)">
+      <SpecificVersion>False</SpecificVersion>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="CommandLine, $(CmdLineInfo)">
+      <SpecificVersion>False</SpecificVersion>
+      <Private>True</Private>
+    </Reference>
+  </ItemGroup>
+
+  <PropertyGroup>
+    <AssemblyName>monogame-patcher</AssemblyName>
+    <OutputType>Exe</OutputType>
+    <OutDir>bin/Release/</OutDir>
+    <OutputPath>bin/Release</OutputPath>
+    <WarningLevel>4</WarningLevel>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+</Project>
diff --git a/pkgs/games/build-support/monogame-patcher/src/test.sh b/pkgs/games/build-support/monogame-patcher/src/test.sh
new file mode 100644
index 00000000..b03fa6cb
--- /dev/null
+++ b/pkgs/games/build-support/monogame-patcher/src/test.sh
@@ -0,0 +1,116 @@
+set -x
+cd "$(mktemp -d)"
+
+cat > "a.cs" <<EOF
+using System;
+
+public class a {
+    public static string replaceMe(string foo) {
+        Console.WriteLine("foo called: " + foo);
+        return "foo";
+    }
+}
+EOF
+
+cat > "b.cs" <<EOF
+using System;
+using System.IO;
+
+public class b {
+    public static string replacement(string bar) {
+        if (bar == "nope" || bar == "yikes")
+            return "nope";
+        Console.WriteLine("bar called: " + bar);
+        return "bar";
+    }
+
+    public static void wrongFileStreamUse() {
+        var fs = new FileStream("write_test.txt", FileMode.Open);
+        if (fs.CanWrite)
+            Console.WriteLine("can write");
+        else
+            Console.WriteLine("can not write");
+    }
+}
+EOF
+
+cat > "c.cs" <<EOF
+using System;
+
+public class c {
+    public static string anotherReplacement(string foobar) {
+        if (foobar == "nope")
+            return "nope";
+        Console.WriteLine("foobar called: " + foobar);
+        return "foobar";
+    }
+}
+EOF
+
+cat > "test1.cs" <<EOF
+class test1 {
+    public static void unrelated() {
+        b.replacement("yikes");
+    }
+
+    public static void Main() {
+        a.replaceMe("xxx");
+        b.replacement("nope");
+        test1.unrelated();
+    }
+}
+EOF
+
+cat > "test2.cs" <<EOF
+class test2 {
+    public static void Main() {
+        b.wrongFileStreamUse();
+    }
+}
+EOF
+
+mkdir subdir
+mcs a.cs -target:library -out:subdir/a.dll
+mcs b.cs -target:library -out:subdir/b.dll
+mcs c.cs -target:library -out:subdir/c.dll
+
+mcs test1.cs -r:subdir/a -r:subdir/b -out:subdir/test1.exe
+mcs test2.cs -r:subdir/a -r:subdir/b -out:subdir/test2.exe
+
+! "$out/bin/monogame-patcher" replace-call -i subdir/test1.exe \
+    "System.String a::replaceMe(System.String)" \
+    "System.String b::notfound(System.String)" \
+    test1 2> /dev/null
+
+"$out/bin/monogame-patcher" replace-call -i subdir/test1.exe \
+    "System.String a::replaceMe(System.String)" \
+    "System.String b::replacement(System.String)" \
+    test1
+
+test "$(mono subdir/test1.exe)" = "bar called: xxx"
+
+"$out/bin/monogame-patcher" replace-call -i subdir/test1.exe -a subdir/c.dll \
+    "System.String b::replacement(System.String)" \
+    "System.String c::anotherReplacement(System.String)" \
+    test1::Main
+
+test "$(mono subdir/test1.exe)" = "foobar called: xxx"
+
+echo foo > write_test.txt
+
+test "$(mono subdir/test2.exe)" = "can write"
+
+"$out/bin/monogame-patcher" fix-filestreams -i subdir/b.dll b
+
+test "$(mono subdir/test2.exe)" = "can not write"
+
+set +e
+"$out/bin/monogame-patcher" --help &> /dev/null
+ret=$?
+set -e
+if [ $ret -eq 0 ]; then
+    echo "Running with --help should give exit status != 0 but was $ret" >&2
+    exit 1
+fi
+
+"$out/bin/monogame-patcher" --help 2>&1 | grep -q fix-filestreams
diff --git a/pkgs/games/build-support/setup-hooks/default.nix b/pkgs/games/build-support/setup-hooks/default.nix
new file mode 100644
index 00000000..56c9139f
--- /dev/null
+++ b/pkgs/games/build-support/setup-hooks/default.nix
@@ -0,0 +1,13 @@
+{ makeSetupHook, libarchive, innoextract }:
+
+{
+  fixFmodHook = makeSetupHook {
+    name = "fix-fmod-hook";
+    deps = [];
+  } ./fix-fmod.sh;
+
+  gogUnpackHook = makeSetupHook {
+    name = "gog-unpack-hook";
+    deps = [ libarchive innoextract ];
+  } ./gog-unpack.sh;
+}
diff --git a/pkgs/games/build-support/setup-hooks/fix-fmod.sh b/pkgs/games/build-support/setup-hooks/fix-fmod.sh
new file mode 100644
index 00000000..bc490a64
--- /dev/null
+++ b/pkgs/games/build-support/setup-hooks/fix-fmod.sh
@@ -0,0 +1,68 @@
+# FMOD tries to run /bin/sh -c 'pulseaudio --check > /dev/null 2>&1', so we
+# need to prevent this by replacing the system() call with a successful return
+# value (0). If someone doesn't have or want to have PulseAudio, FMOD still
+# falls back to ALSA if it can't load libpulse-simple.so.
+_doPatchFmod() {
+    set -e
+    set -o pipefail
+
+    # Let's make sure it's really the affected FMOD version:
+    objdump -T "$1" 2> /dev/null | grep -q "\\<FMOD_System_Init\\>"
+    grep -qF "pulseaudio --check" "$1"
+
+    case "$(objdump -f "$1" | sed -n -e 's/^architecture: *//p')" in
+        i386:x86-64,*)
+            local addr="$(objdump -d "$1" | sed -n -e '
+                /callq.*system@plt/s/^ *\([^:]\+\).*/\1/p
+            ')"
+
+            # This is quite easy, just replace the system() call with XOR EAX
+            # so we get a return value of 0 and pad the rest with NOP.
+            local offset=$(("0x$addr"))
+            ( printf '\x31\xc0'     # XOR the EAX register
+              printf '\x90\x90\x90' # Fill with NOPs
+            ) | dd of="$1" obs=1 seek=$offset conv=notrunc status=none
+            ;;
+        i386,*)
+            local relocSystem="$(readelf -r "$1" | sed -n -e '
+                /system@/s/^0*\([^ ]\+\).*/\1/p
+            ')"
+            local addr="$(objdump -d "$1" | sed -n -e '
+                /call *'"$relocSystem"' /s/^ *\([^:]\+\).*/\1/p
+            ')"
+
+            # For the 32 bit library it's not so easy as the 4 bytes coming
+            # after the CALL opcode will be replaced by the dynamic linker, so
+            # we just XOR the EAX register with the relocation address and
+            # replace the TEST opcode afterwards.
+            local offset=$(("0x$addr"))
+            ( printf '\x35\xfc\xff\xff\xff' # XOR EAX with the relocation addr
+              printf '\x39\xc0'             # CMP EAX, EAX
+            ) | dd of="$1" obs=1 seek=$offset conv=notrunc status=none
+            ;;
+        *) return ;;
+    esac
+
+    # Only needed if the library is used with dlopen().
+    if [ -n "$runtimeDependencies" ]; then
+        local dep rpath="$(patchelf --print-rpath "$1")"
+        for dep in $runtimeDependencies; do
+          rpath="$rpath''${rpath:+:}$dep/lib"
+        done
+        patchelf --set-rpath "$rpath" "$1"
+    fi
+
+    echo "$1: removed call to system()" >&2
+}
+
+patchFmod() {
+    export -f _doPatchFmod
+    echo "patching out system() calls in FMOD" >&2
+    find "$prefix" \( -iname '*fmod*.so' -o -iname '*fmod*.so.*' \) \
+        -exec "$SHELL" -c '_doPatchFmod "$1"' -- {} \;
+}
+
+# Needs to come after the setup hook of patchelf.
+postFixupHooks+=(
+    'for output in $outputs; do prefix="${!output}" patchFmod; done'
+)
diff --git a/pkgs/games/build-support/setup-hooks/gog-unpack.sh b/pkgs/games/build-support/setup-hooks/gog-unpack.sh
new file mode 100644
index 00000000..8b70803c
--- /dev/null
+++ b/pkgs/games/build-support/setup-hooks/gog-unpack.sh
@@ -0,0 +1,71 @@
+unpackCmdHooks+=(_tryUnpackGogMakeSelf _tryUnpackGogInnoSetup)
+
+_tryUnpackGogMakeSelf() {
+  # Make sure it's really a Makeself installer with GOG.com modifications.
+  sed -n -e '1,/^\([^#]\| *\)$/ {
+    /This script.*Makeself/ {
+      n; /GOG\.com/q
+    }
+    200q1 # This is getting too long, so quit immediately.
+    b
+  }
+  q1' "$curSrc" || return 1
+
+  # The file consists of a shell script at the top, followed by a tarball with
+  # the installer and after that tarball, the actual ZIP file with the game
+  # data follows. So we need to calculate the offsets accordingly, which
+  # luckily are dumped using --dumpconf (we only get the sizes, but we can
+  # infer the starting offset for the ZIP file using those).
+  local zipfileOffset="$(
+    eval "$($SHELL "$curSrc" --dumpconf)"
+    declare -i offset=1
+    offset+="$(head -n $(($OLDSKIP - 1)) "$curSrc" | wc -c)"
+    for fs in $filesizes; do
+      offset+="$fs"
+    done
+    echo "$offset"
+  )"
+
+  # Unfortunately bsdtar exits with failure if one of the patterns specified
+  # using the --include flag doesn't match. However, if the desktop icon is
+  # missing it's not the end of the world, so we need to find another way to
+  # make it happen without bsdtar returning a failure.
+  #
+  # Let's introduce -s, which is used to substitute the paths. While it may
+  # sound eligible to be used in conjunction --include, it's only really useful
+  # for our case if the inclusion patterns would be matched _after_ the
+  # substitiotions. Unfortunately, they're matched before the substitions.
+  #
+  # So what we do instead is rewrite *everything* that we want to include into
+  # a special path "/_/game" and rewrite everything that doesn't begin with /
+  # into "skip". We're (ab)using the fact here that files coming from the
+  # archive never start with "/", but during the substitutions the leading
+  # slash isn't stripped.
+  #
+  # In the end the resulting paths are normalized, so "/..." will be turned
+  # into "./...", so all we need to do in the end is to strip 2 components from
+  # the resulting path. This discards every path that has been renamed to
+  # "skip".
+  tail -c"+$zipfileOffset" "$curSrc" | bsdtar -xf - \
+    -s '!^data/noarch/game/\(.*\)$!/_/game/\1!' \
+    -s '!^data/noarch/support/icon\.png$!/_/game/xdg-icon.png!' \
+    -s '!^[^/].*!skip!' --strip-components=2
+}
+
+_tryUnpackGogInnoSetup() {
+  innoextract -i "$curSrc" &> /dev/null || return 1
+
+  local -a unpackArgs=()
+  if [ -n "$innoExtractOnly" ]; then
+    local i
+    for i in $innoExtractOnly; do
+      unpackArgs+=("--include" "$i")
+    done
+  fi
+
+  if [ -z "$innoExtractKeepCase" ]; then
+    unpackArgs+=("-L")
+  fi
+
+  innoextract -s "${unpackArgs[@]}" -m "$curSrc"
+}
diff --git a/pkgs/games/default.nix b/pkgs/games/default.nix
new file mode 100644
index 00000000..71ccdf5e
--- /dev/null
+++ b/pkgs/games/default.nix
@@ -0,0 +1,51 @@
+{ config ? null, pkgs ? import <nixpkgs> {}
+, pkgsi686Linux ? pkgs.pkgsi686Linux
+}:
+
+let
+  configFilePath = let
+    xdgConfig = builtins.getEnv "XDG_CONFIG_HOME";
+    fallback = "${builtins.getEnv "HOME"}/.config";
+    basedir = if xdgConfig == "" then fallback else xdgConfig;
+  in "${basedir}/nixgames.nix";
+
+  configFile = if !builtins.pathExists configFilePath then throw ''
+    The config file "${configFilePath}" doesn't exist! Be sure to create it and
+    put your credentials in it, for example to use HumbleBundle games:
+
+    {
+      humblebundle.email = "fancyuser@example.com";
+      humblebundle.password = "my_super_secret_password";
+    }
+  '' else configFilePath;
+
+  mkBuildSupport = super: let
+    self = import ./build-support {
+      inherit (super) config;
+      callPackage = pkgs.lib.callPackageWith (super // self);
+      callPackages = pkgs.lib.callPackagesWith (super // self);
+    };
+  in self;
+
+  baseModule = { lib, ... }: {
+    options = {
+      packages = lib.mkOption {
+        type = lib.types.attrsOf lib.types.unspecified;
+        default = {};
+        description = "Available collections of games.";
+      };
+    };
+
+    config._module.args.pkgs = pkgs // (mkBuildSupport pkgs) // {
+      pkgsi686Linux = pkgsi686Linux // (mkBuildSupport pkgsi686Linux);
+    };
+  };
+
+  packages = (pkgs.lib.evalModules {
+    modules = [
+      (if config == null then configFile else config)
+      baseModule ./humblebundle ./steam ./itch ./gog
+    ];
+  }).config.packages;
+
+in packages // mkBuildSupport pkgs
diff --git a/pkgs/games/gog/albion/cdpath-is-gamedir.patch b/pkgs/games/gog/albion/cdpath-is-gamedir.patch
new file mode 100644
index 00000000..36728a11
--- /dev/null
+++ b/pkgs/games/gog/albion/cdpath-is-gamedir.patch
@@ -0,0 +1,66 @@
+diff --git a/games/Albion/SR-Main/main.c b/games/Albion/SR-Main/main.c
+index c9c3125..ad4c367 100644
+--- a/games/Albion/SR-Main/main.c
++++ b/games/Albion/SR-Main/main.c
+@@ -529,44 +529,6 @@ static void Game_BuildRTable(void)
+     }
+ }
+ 
+-static void Game_ReadCDPath(void)
+-{
+-    char str[8192];
+-    int len;
+-    FILE *f;
+-
+-    f = Game_fopen("SETUP.INI", "rt");
+-
+-    if (f != NULL)
+-    {
+-        while (!feof(f))
+-        {
+-            str[0] = 0;
+-            fscanf(f, "%8192[^\n]\n", str);
+-            if (strncasecmp(str, "SOURCE_PATH=", 12) == 0)
+-            {
+-                strcpy(Albion_CDPath, &(str[12]));
+-                len = strlen(Albion_CDPath);
+-                if ((len != 0) && (Albion_CDPath[len - 1] == '\r'))
+-                {
+-                    Albion_CDPath[len - 1] = 0;
+-                    len--;
+-                }
+-                if (len != 0)
+-                {
+-                    if (Albion_CDPath[len - 1] != '\\')
+-                    {
+-                        Albion_CDPath[len] = '\\';
+-                        Albion_CDPath[len + 1] = 0;
+-                    }
+-                    break;
+-                }
+-            }
+-        }
+-        fclose(f);
+-    }
+-}
+-
+ static uint32_t calculate_crc(uint8_t *buf, unsigned int size)
+ {
+ #define POLYNOMIAL ((uint32_t)0xEDB88320)
+@@ -730,7 +692,7 @@ static int Game_Initialize(void)
+     }
+ 
+ 
+-    Albion_CDPath[0] = 0;
++    strcpy(Albion_CDPath, Game_Directory);
+     Albion_Font = NULL;
+     Albion_Font_Lang = AL_UNKNOWN;
+     Temp_Font_Data = NULL;
+@@ -1385,7 +1347,6 @@ int main (int argc, char *argv[])
+     }
+ 
+     Game_ReadConfig();
+-    Game_ReadCDPath();
+     Game_ReadFontData();
+ 
+     Game_Initialize2();
diff --git a/pkgs/games/gog/albion/config.patch b/pkgs/games/gog/albion/config.patch
new file mode 100644
index 00000000..6bac229c
--- /dev/null
+++ b/pkgs/games/gog/albion/config.patch
@@ -0,0 +1,19 @@
+diff --git a/games/Albion/release/linux/Albion.cfg b/games/Albion/release/linux/Albion.cfg
+index 50d4327..8c901f1 100644
+--- a/games/Albion/release/linux/Albion.cfg
++++ b/games/Albion/release/linux/Albion.cfg
+@@ -50,11 +50,11 @@ Audio_MIDI_Device=
+ #     - this setting renders the 3d part of the game in the native resolution instead of rendering it in the original resolution and then scaling it
+ #     - there are some minor issues, read the readme for more information
+ # Display_MouseCursor=normal/minimal/none - shape of SDL mouse cursor in window mode
+-Display_ScaledWidth=720
+-Display_ScaledHeight=480
++Display_ScaledWidth=1280
++Display_ScaledHeight=960
+ Display_Fullscreen=no
+ Display_Enhanced_3D_Rendering=on
+-Display_MouseCursor=normal
++Display_MouseCursor=none
+ 
+ 
+ # Screenshot settings
diff --git a/pkgs/games/gog/albion/default.nix b/pkgs/games/gog/albion/default.nix
new file mode 100644
index 00000000..a7493712
--- /dev/null
+++ b/pkgs/games/gog/albion/default.nix
@@ -0,0 +1,187 @@
+{ stdenv, lib, buildSandbox, fetchGog, gogUnpackHook, fetchzip
+, SDL2, SDL2_mixer, bchunk, p7zip, alsaLib, writeText, makeWrapper, libGL
+
+# For static recompilation
+, fetchFromGitHub, scons, judy, python, nasm, autoreconfHook
+
+, language ? "en"
+}:
+
+let
+  version = "1.6.1";
+
+  staticRecompilerSource = fetchFromGitHub {
+    owner = "M-HT";
+    repo = "SR";
+    rev = "albion_v${version}";
+    sha256 = "0yspgssfk5xbrs5krq0sin561rgb0fva4hk7mlxlcrvs0xpqf5z8";
+  };
+
+  mkPatchedWildMidi = variant: stdenv.mkDerivation {
+    name = "${variant}-0.2.3.5patched";
+    src = "${staticRecompilerSource}/midi-libs/${variant}-0.2.3.5svn";
+    nativeBuildInputs = [ autoreconfHook ];
+    buildInputs = [ alsaLib ];
+    patches = [ ./wildmidi-build-fixes.patch ];
+    postPatch = ''
+      sed -i -e '/^CFLAGS/s/-pedantic//' configure.ac
+      sed -i -e '/^wildmidi_libs *=/s!\$(top_builddir)/src/!!' src/Makefile.am
+    '';
+  };
+
+  compileMidiPlugin = variant: let
+    commonDrvAttrs = rec {
+      name = "midi-plugin-${variant}";
+      soname = "midi-${variant}";
+      sourceFile = "${soname}.c";
+      midiLib = "WildMidi";
+
+      src = "${staticRecompilerSource}/midi-plugins";
+
+      buildInputs = lib.singleton (mkPatchedWildMidi variant);
+
+      timidityCfg = let
+        gusPatches = lib.overrideDerivation (fetchzip {
+          url = "http://sebt3.openpandora.org/pnd/timidity_midi_installer.pnd";
+          sha256 = "0nznac8lxcbj0fwbg0njlnh3ysa3d3c5i24n2cw0yv5yqji4cdsb";
+          stripRoot = false;
+        }) (lib.const { unpackCmd = "${p7zip}/bin/7z x \"$curSrc\""; });
+      in writeText "timidity-albion.cfg" ''
+        dir ${gusPatches}/eawpats
+        source ${gusPatches}/eawpats/sounds.cfg
+      '';
+
+      postPatch = ''
+        sed -i -e 's!getenv("TIMIDITY_CFG")!"'"$timidityCfg"'"!' \
+          midi-wildmidi.c
+      '';
+
+      buildPhase = ''
+        gcc -shared -Wl,-soname,"$soname.so" -I. \
+          -o "$soname.so" -fpic -m32 -O2 -Wall \
+          "$sourceFile" -l"$midiLib"
+      '';
+
+      installPhase = ''
+        install -vD "$soname.so" "$out/lib/$soname.so"
+      '';
+    };
+
+    extraAttrs = lib.optionalAttrs (variant == "wildmidiA") {
+      soname = "midiA-wildmidi";
+      sourceFile = "albion/midiA-wildmidi.c";
+      midiLib = "WildMidiA";
+    };
+
+  in stdenv.mkDerivation (commonDrvAttrs // extraAttrs);
+
+  udis86 = stdenv.mkDerivation {
+    name = "udis86";
+    src = "${staticRecompilerSource}/SR/udis86-1.6";
+    postPatch = "chmod +x configure mkinstalldirs";
+    preInstall = "mkdir -p \"$out/lib\" \"$out/bin\"";
+  };
+
+  gameData = stdenv.mkDerivation rec {
+    name = "albion-game-data-${version}";
+    version = "3";
+
+    src = fetchGog {
+      productId = 1436955815;
+      downloadName = "${language}1installer1";
+      sha256 = ({
+        de = "0ylhma70kcj255i03gy5xa3adb8hfw2xpk1m2pp5880aqkmr06k7";
+        en = "0x0s2q0x7kjz6qfhb9qs5d959caijiinpc7xv4rx9n7mmb7xlh5m";
+      }).${language};
+    };
+
+    outputs = [ "out" "dev" ];
+
+    nativeBuildInputs = [ gogUnpackHook ];
+    innoExtractOnly = [ "game.gog" "game.ins" "MAIN.EXE" "SETUP.INI" ];
+    innoExtractKeepCase = true;
+
+    phases = [ "unpackPhase" "patchPhase" "installPhase" ];
+
+    patchPhase = ''
+      sed -i -e '
+        s,^SOURCE_PATH=.*,SOURCE_PATH=C:\\,
+        s/^\(MODE_[^=]*=\)N$/\1Y/
+      ' SETUP.INI
+    '';
+    installPhase = ''
+      ${bchunk}/bin/bchunk game.gog game.ins game_cd
+      ${p7zip}/bin/7z x game_cd01.iso ALBION
+      mv ALBION "$out"
+      install -vD -m 0644 SETUP.INI "$out/setup.ini"
+      install -vD -m 0644 MAIN.EXE "$dev/albion_main.exe"
+    '';
+  };
+
+in buildSandbox (stdenv.mkDerivation {
+  name = "albion-${version}";
+  inherit version;
+
+  src = staticRecompilerSource;
+
+  patches = [
+    ./scons.patch ./xdg-paths.patch ./config.patch ./sdl2.patch
+    ./error-log-stderr.patch ./cdpath-is-gamedir.patch
+    ./storepaths.patch
+  ];
+
+  wildmidi = compileMidiPlugin "wildmidi";
+  wildmidiA = compileMidiPlugin "wildmidiA";
+
+  postPatch = ''
+    substituteInPlace games/Albion/SR-Main/main.c \
+      --subst-var-by GAME_CONFIG_FILE "$out/etc/albion.cfg" \
+      --subst-var-by GAME_DATA_PATH ${lib.escapeShellArg gameData.out}
+
+    substituteInPlace games/Albion/SR-Main/virtualfs.c \
+      --subst-var-by SETUP_INI_PATH "${gameData.out}/setup.ini"
+
+    substituteInPlace games/Albion/SR-Main/Albion-music-midiplugin.c \
+      --replace ./midi-wildmidi.so "$wildmidi/lib/midi-wildmidi.so" \
+      --replace ./midiA-wildmidi.so "$wildmidiA/lib/midiA-wildmidi.so" \
+  '';
+
+  nativeBuildInputs = [ scons judy python nasm udis86 makeWrapper ];
+  buildInputs = [ SDL2 SDL2_mixer ];
+
+  NIX_CFLAGS_COMPILE = "-I${lib.getDev SDL2}/include/SDL2";
+
+  buildPhase = ''
+    scons -C SR debug=1
+
+    mkdir tmp
+    pushd tmp
+    cp ../SR-games/Albion/SR/x86/*.sci .
+    ../SR/SR.exe ${gameData.dev}/albion_main.exe Albion-main.asm
+    rm *.sci
+    python ../SR-games/Albion/SR/compact_source.py
+    nasm -felf -dELF -O1 -w+orphan-labels -w-number-overflow \
+      -i../SR-games/Albion/SR/x86/ Albion-main_linux.asm 2> a.a || :
+    python ../SR-games/Albion/SR/repair_short_jumps.py
+    popd
+
+    mv tmp/seg*.inc tmp/Albion-main.asm tmp/Albion-main_linux.asm \
+      games/Albion/SR-Main/x86
+    rm -r tmp
+
+    scons -C games/Albion/SR-Main debug=1 device=pc-linux sdl2=1
+  '';
+
+  installPhase = ''
+    install -vD -m 0644 games/Albion/release/linux/Albion.cfg \
+      "$out/etc/albion.cfg"
+    install -vD games/Albion/SR-Main/SR-Main "$out/bin/albion"
+
+    # XXX: Temporary workaround, because SDL tries to dlopen() libGL.
+    wrapProgram "$out/bin/albion" \
+      --set SDL_OPENGL_LIBRARY ${lib.escapeShellArg "${libGL}/lib/libGL.so"}
+  '';
+}) {
+  paths.required = [ "$XDG_DATA_HOME/albion" "$XDG_CONFIG_HOME/albion" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" ];
+}
diff --git a/pkgs/games/gog/albion/error-log-stderr.patch b/pkgs/games/gog/albion/error-log-stderr.patch
new file mode 100644
index 00000000..9819f1f1
--- /dev/null
+++ b/pkgs/games/gog/albion/error-log-stderr.patch
@@ -0,0 +1,100 @@
+diff --git a/games/Albion/SR-Main/Albion-proc-vfs.c b/games/Albion/SR-Main/Albion-proc-vfs.c
+index c3d2d4f..faa90e7 100644
+--- a/games/Albion/SR-Main/Albion-proc-vfs.c
++++ b/games/Albion/SR-Main/Albion-proc-vfs.c
+@@ -232,6 +232,8 @@ FILE *Game_fopen(const char *filename, const char *mode)
+     fprintf(stderr, "fopen: original name: %s\n", filename);
+ #endif
+ 
++    if (strcasecmp(filename, "error.log") == 0) return stderr;
++
+     vfs_err = vfs_get_real_name(filename, (char *) &temp_str, &realdir);
+ 
+ #if defined(__DEBUG__)
+@@ -260,6 +262,8 @@ int Game_open(const char *pathname, int flags, mode_t mode)
+     fprintf(stderr, "open: original name: %s\n", pathname);
+ #endif
+ 
++    if (strcasecmp(pathname, "error.log") == 0) return STDERR_FILENO;
++
+     vfs_err = vfs_get_real_name(pathname, (char *) &temp_str, &realdir);
+ 
+ #if defined(__DEBUG__)
+@@ -726,6 +730,16 @@ int Game_closedir(struct watcom_dirent *dirp)
+     return 0;
+ }
+ 
++int Game_close(int fd)
++{
++    return fd == STDERR_FILENO ? 0 : close(fd);
++}
++
++int Game_fclose(FILE *stream)
++{
++    return stream == stderr ? 0 : fclose(stream);
++}
++
+ static void Conv_find(struct watcom_find_t *buffer, struct watcom_dirent *direntp)
+ {
+     // file attributes
+diff --git a/games/Albion/SR-Main/Albion-proc-vfs.h b/games/Albion/SR-Main/Albion-proc-vfs.h
+index 0cf4491..2e16671 100644
+--- a/games/Albion/SR-Main/Albion-proc-vfs.h
++++ b/games/Albion/SR-Main/Albion-proc-vfs.h
+@@ -110,6 +110,8 @@ extern int Game_rename(const char *oldpath, const char *newpath);
+ extern struct watcom_dirent *Game_opendir(const char *dirname);
+ extern struct watcom_dirent *Game_readdir(struct watcom_dirent *dirp);
+ extern int Game_closedir(struct watcom_dirent *dirp);
++extern int Game_close(int fd);
++extern int Game_fclose(FILE *stream);
+ extern uint32_t Game_dos_findfirst(const char *path, const uint32_t attributes, struct watcom_find_t *buffer);
+ extern uint32_t Game_dos_findnext(struct watcom_find_t *buffer);
+ extern uint32_t Game_dos_findclose(struct watcom_find_t *buffer);
+diff --git a/games/Albion/SR-Main/x86/SR-asm-calls.asm b/games/Albion/SR-Main/x86/SR-asm-calls.asm
+index 3cb2cc8..e1741dc 100644
+--- a/games/Albion/SR-Main/x86/SR-asm-calls.asm
++++ b/games/Albion/SR-Main/x86/SR-asm-calls.asm
+@@ -71,6 +71,8 @@
+     %define Game_chdir _Game_chdir
+     %define close _close
+     %define Game_closedir _Game_closedir
++    %define Game_close _Game_close
++    %define Game_fclose _Game_fclose
+     %define ctime _ctime
+     %define Game_dos_findclose _Game_dos_findclose
+     %define Game_dos_findnext _Game_dos_findnext
+@@ -171,14 +173,14 @@ extern Game_WaitFor2ndVerticalRetrace
+ ; 1 param
+ extern asctime
+ extern Game_chdir
+-extern close
++extern Game_close
+ extern Game_closedir
+ extern ctime
+ extern Game_dos_findclose
+ extern Game_dos_findnext
+ extern Game_dos_getvect
+ extern Game_ExitMain_Asm
+-extern fclose
++extern Game_fclose
+ extern Game_filelength
+ extern free
+ extern ftime
+@@ -798,7 +800,7 @@ SR_j___close:
+ 
+ ; eax = int handle
+ 
+-        Game_Call_Asm_Reg1 close,'get_errno_val'
++        Game_Call_Asm_Reg1 Game_close,'get_errno_val'
+ 
+ ; end procedure SR___close
+ 
+@@ -875,7 +877,7 @@ SR_fclose:
+ 
+ ; eax = FILE *fp
+ 
+-        Game_Call_Asm_Reg1 fclose,'get_errno_val'
++        Game_Call_Asm_Reg1 Game_fclose,'get_errno_val'
+ 
+ ; end procedure SR_fclose
+ 
diff --git a/pkgs/games/gog/albion/scons.patch b/pkgs/games/gog/albion/scons.patch
new file mode 100644
index 00000000..5a1c0d7b
--- /dev/null
+++ b/pkgs/games/gog/albion/scons.patch
@@ -0,0 +1,74 @@
+diff --git a/SR/SConstruct b/SR/SConstruct
+index 2fb2874..f2fb527 100644
+--- a/SR/SConstruct
++++ b/SR/SConstruct
+@@ -20,6 +20,8 @@
+ #  SOFTWARE.
+ #
+ 
++import os
++
+ udis86_path = './udis86-1.6/'
+ 
+ # set help text
+@@ -30,7 +32,8 @@ Help(vars.GenerateHelpText(env))
+ debug = env['debug']
+ 
+ # default settings
+-env = Environment(CCFLAGS      = '-O2',
++env = Environment(ENV          = os.environ,
++                  CCFLAGS      = '-O2',
+                   CPPPATH      = '.',
+                   INCPREFIX    = '-I' + udis86_path,
+                   LIBPATH      = 'libudis86',
+diff --git a/games/Albion/SR-Main/SConstruct b/games/Albion/SR-Main/SConstruct
+index 96bbefb..6743470 100644
+--- a/games/Albion/SR-Main/SConstruct
++++ b/games/Albion/SR-Main/SConstruct
+@@ -50,12 +50,14 @@ Help(vars.GenerateHelpText(env))
+ if device == 'pc-linux':
+     # default settings
+     if sdl2 > 0:
+-        env = Environment(CCFLAGS      = '-m32 -O2 -DUSE_SDL2',
++        env = Environment(ENV          = os.environ,
++                          CCFLAGS      = '-m32 -O2 -DUSE_SDL2',
+                           LINKFLAGS    = '-m32',
+                           LIBS         = ['SDL2_mixer', 'SDL2', 'pthread', 'm', 'dl']
+                          )
+     else:
+-        env = Environment(CCFLAGS      = '-m32 -O2 -DALLOW_OPENGL',
++        env = Environment(ENV          = os.environ,
++                          CCFLAGS      = '-m32 -O2 -DALLOW_OPENGL',
+                           LINKFLAGS    = '-m32',
+                           LIBS         = ['SDL_mixer', 'SDL', 'pthread', 'm', 'dl', 'GL']
+                          )
+diff --git a/games/Albion/SR-Main/x86/SConscript b/games/Albion/SR-Main/x86/SConscript
+index c88c7e9..848efa8 100644
+--- a/games/Albion/SR-Main/x86/SConscript
++++ b/games/Albion/SR-Main/x86/SConscript
+@@ -20,6 +20,7 @@
+ #  SOFTWARE.
+ #
+ 
++import os
+ import re
+ 
+ Import('device')
+@@ -38,13 +39,13 @@ nasmscan = Scanner(function = nasmfile_scan,
+ SourceFileScanner.add_scanner('.asm', nasmscan)
+ 
+ if device == 'pc-linux':
+-    env = Environment(tools=['nasm'], ASFLAGS = ' -felf -dELF -Ox -w+orphan-labels -w-number-overflow -ix86/')
+-    env2 = Environment(tools=['nasm'], ASFLAGS = ' -felf -dELF -O1 -w+orphan-labels -w-number-overflow -ix86/')
++    env = Environment(ENV=os.environ, tools=['nasm'], ASFLAGS = ' -felf -dELF -Ox -w+orphan-labels -w-number-overflow -ix86/')
++    env2 = Environment(ENV=os.environ, tools=['nasm'], ASFLAGS = ' -felf -dELF -O1 -w+orphan-labels -w-number-overflow -ix86/')
+ 
+     obj = env2.Object('Albion-main_linux.asm')
+ else:
+-    env = Environment(tools=['nasm'], ASFLAGS = ' -fwin32 -Ox -w+orphan-labels -w-number-overflow -ix86/')
+-    env2 = Environment(tools=['nasm'], ASFLAGS = ' -fwin32 -O1 -w+orphan-labels -w-number-overflow -ix86/')
++    env = Environment(ENV=os.environ, tools=['nasm'], ASFLAGS = ' -fwin32 -Ox -w+orphan-labels -w-number-overflow -ix86/')
++    env2 = Environment(ENV=os.environ, tools=['nasm'], ASFLAGS = ' -fwin32 -O1 -w+orphan-labels -w-number-overflow -ix86/')
+ 
+     obj = env2.Object('Albion-main.asm')
+ 
diff --git a/pkgs/games/gog/albion/sdl2.patch b/pkgs/games/gog/albion/sdl2.patch
new file mode 100644
index 00000000..6b4bc93f
--- /dev/null
+++ b/pkgs/games/gog/albion/sdl2.patch
@@ -0,0 +1,60 @@
+diff --git a/games/Albion/SR-Main/Albion-proc-events.c b/games/Albion/SR-Main/Albion-proc-events.c
+index c323530..97eedd1 100644
+--- a/games/Albion/SR-Main/Albion-proc-events.c
++++ b/games/Albion/SR-Main/Albion-proc-events.c
+@@ -893,19 +893,19 @@ void Game_ProcessKEvents(void)
+                     else goto _after_switch1;
+             #endif
+                 }
+-                else if ((cevent->key.keysym.unicode > 0) && (cevent->key.keysym.unicode < 128))
++                else if ((cevent->key.keysym.sym > 0) && (cevent->key.keysym.sym < 128))
+                 {
+-                    scancode = scancode_table[cevent->key.keysym.unicode];
+-                    ascii_code = cevent->key.keysym.unicode;
++                    scancode = scancode_table[cevent->key.keysym.sym];
++                    ascii_code = cevent->key.keysym.sym;
+                 }
+-                else if (cevent->key.keysym.unicode != 0)
++                else if (cevent->key.keysym.sym != 0)
+                 {
+                     scancode = 0;
+                     ascii_code = 0;
+ 
+                     if ((ascii_code == 0) && (Albion_Font_Lang != AL_UNKNOWN))
+                     {
+-                        switch (cevent->key.keysym.unicode)
++                        switch (cevent->key.keysym.sym)
+                         {
+                             case 0x00E4: // ä
+                                 ascii_code = 0x84;
+@@ -935,7 +935,7 @@ void Game_ProcessKEvents(void)
+ 
+                     if ((ascii_code == 0) && (Albion_Font_Lang == AL_ENG_FRE))
+                     {
+-                        switch (cevent->key.keysym.unicode)
++                        switch (cevent->key.keysym.sym)
+                         {
+                             case 0x00E9: // é
+                                 ascii_code = 0x82;
+@@ -998,7 +998,7 @@ void Game_ProcessKEvents(void)
+ 
+                     if ((ascii_code == 0) && (Albion_Font_Lang == AL_CZE))
+                     {
+-                        switch (cevent->key.keysym.unicode)
++                        switch (cevent->key.keysym.sym)
+                         {
+                             case 0x00E9: // é
+                                 ascii_code = 0x82;
+diff --git a/games/Albion/SR-Main/main.c b/games/Albion/SR-Main/main.c
+index c9c3125..0d32bcb 100644
+--- a/games/Albion/SR-Main/main.c
++++ b/games/Albion/SR-Main/main.c
+@@ -930,8 +930,6 @@ static void Game_Initialize2(void)
+     Init_Audio2();
+     Init_Input2();
+ 
+-    SDL_EnableUNICODE(1);
+-
+     Game_VideoAspectX = (360 << 16) / Picture_Width;
+     Game_VideoAspectY = (240 << 16) / Picture_Height;
+ 
diff --git a/pkgs/games/gog/albion/storepaths.patch b/pkgs/games/gog/albion/storepaths.patch
new file mode 100644
index 00000000..c3228cb0
--- /dev/null
+++ b/pkgs/games/gog/albion/storepaths.patch
@@ -0,0 +1,15 @@
+diff --git a/games/Albion/SR-Main/main.c b/games/Albion/SR-Main/main.c
+index c9c3125..b186235 100644
+--- a/games/Albion/SR-Main/main.c
++++ b/games/Albion/SR-Main/main.c
+@@ -1335,8 +1335,8 @@ static void Game_Event_Loop(void)
+ 
+ int main (int argc, char *argv[])
+ {
+-    Game_ConfigFilename[0] = 0;
+-    Game_Directory[0] = 0;
++    strcpy(Game_ConfigFilename, "@GAME_CONFIG_FILE@");
++    strcpy(Game_Directory, "@GAME_DATA_PATH@");
+ 
+     //senquack - can now specify config file on command line
+     // read parameters
diff --git a/pkgs/games/gog/albion/wildmidi-build-fixes.patch b/pkgs/games/gog/albion/wildmidi-build-fixes.patch
new file mode 100644
index 00000000..204637ba
--- /dev/null
+++ b/pkgs/games/gog/albion/wildmidi-build-fixes.patch
@@ -0,0 +1,25 @@
+diff --git a/src/wildmidi.c b/src/wildmidi.c
+index 87a8861..6401b9b 100644
+--- a/src/wildmidi.c
++++ b/src/wildmidi.c
+@@ -786,7 +786,7 @@ main (int argc, char **argv) {
+ 
+ #ifndef _WIN32
+ 	int my_tty;
+-	struct termios _tty;
++	struct termios _tty = {0};
+ 	tcflag_t _res_oflg = _tty.c_oflag;
+ 	tcflag_t _res_lflg = _tty.c_lflag;
+ 
+diff --git a/src/wildmidi_lib.c b/src/wildmidi_lib.c
+index 61df0cd..d527c94 100644
+--- a/src/wildmidi_lib.c
++++ b/src/wildmidi_lib.c
+@@ -1852,6 +1852,7 @@ midi_setup_control (struct _mdi *mdi, unsigned char channel, unsigned char contr
+         case 98:
+         case 99:
+             tmp_event = *do_control_non_registered_param;
++	    break;
+         case 100:
+             tmp_event = *do_control_registered_param_fine;
+             break;
diff --git a/pkgs/games/gog/albion/xdg-paths.patch b/pkgs/games/gog/albion/xdg-paths.patch
new file mode 100644
index 00000000..13ba4a1c
--- /dev/null
+++ b/pkgs/games/gog/albion/xdg-paths.patch
@@ -0,0 +1,259 @@
+diff --git a/games/Albion/SR-Main/virtualfs.c b/games/Albion/SR-Main/virtualfs.c
+index 34e0544..7d3acec 100644
+--- a/games/Albion/SR-Main/virtualfs.c
++++ b/games/Albion/SR-Main/virtualfs.c
+@@ -22,7 +22,9 @@
+  *
+  */
+ 
++#define _GNU_SOURCE
+ #define _FILE_OFFSET_BITS 64
++#include <fcntl.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <malloc.h>
+@@ -283,6 +285,177 @@ void vfs_visit_dir(file_entry *vdir)
+     vdir->dir_visited = 1;
+ }
+ 
++#define CONCAT_ENV(path) \
++    if (asprintf(&result, "%s/%s", env, path) == -1) { \
++        perror("asprintf"); \
++        exit(1); \
++    }
++
++#define DEFINE_XDG_GETTER(fun_name, envar, fallback) \
++    static char *fun_name(void) { \
++        const char *env; \
++        static char *result = NULL; \
++        if (result == NULL) { \
++            if ((env = getenv(envar)) != NULL) { \
++                CONCAT_ENV("albion"); \
++            } else if ((env = getenv("HOME")) != NULL) { \
++                CONCAT_ENV(fallback "/albion"); \
++            } else { \
++                fputs("Unable to determine " envar " or HOME.\n", stderr); \
++                exit(1); \
++            } \
++        } \
++        return result; \
++    }
++
++DEFINE_XDG_GETTER(getDataDir, "XDG_DATA_HOME", ".local/share");
++DEFINE_XDG_GETTER(getConfigDir, "XDG_CONFIG_HOME", ".config");
++
++static int makeDirs(const char *path)
++{
++    char *buf, *p;
++
++    if (*path == '\0')
++        return 1;
++
++    if ((buf = strdup(path)) == NULL)
++        return 1;
++
++    for (p = buf + 1; *p != '\0'; p++) {
++        if (*p != '/') continue;
++        *p = '\0';
++        mkdir(buf, 0777);
++        *p = '/';
++    }
++
++    mkdir(buf, 0777);
++
++    free(buf);
++    return 0;
++}
++
++typedef struct {
++    file_entry *dir;
++    const char *root;
++} unix_dir_cache_t;
++
++unix_dir_cache_t *unixDirCache[100];
++
++static void setUnixDir(const char *root, file_entry **dir)
++{
++    int i;
++    file_entry *newdir;
++
++    if (dir == NULL)
++        return;
++
++    for (i = 0; unixDirCache[i] != NULL; ++i) {
++        if (strcmp(unixDirCache[i]->root, root) == 0) {
++            *dir = unixDirCache[i]->dir;
++            return;
++        }
++    }
++
++    if ((newdir = (file_entry *)malloc(sizeof(file_entry))) == NULL)
++        return;
++
++    memset(newdir, 0, sizeof(file_entry));
++
++    newdir->dos_name[0] = 'X';
++    newdir->dos_name[1] = ':';
++    newdir->dos_name[2] = '\0';
++
++    newdir->real_name[0] = '.';
++    newdir->real_name[1] = '\0';
++
++    newdir->dos_fullname = strdup(newdir->dos_name);
++    newdir->real_fullname = strdup(root);
++
++    newdir->attributes = 1;
++    newdir->dir_visited = 0;
++
++    newdir->parent = newdir;
++    newdir->next = NULL;
++    newdir->prev = NULL;
++    newdir->first_child = NULL;
++    *dir = newdir;
++
++    unixDirCache[i] = (unix_dir_cache_t*)malloc(sizeof(unix_dir_cache_t));
++    if (unixDirCache[i] != NULL) {
++        makeDirs(root);
++        unixDirCache[i]->root = strdup(root);
++        unixDirCache[i]->dir = newdir;
++    }
++}
++
++static const char *manglePath(const char *xdg_path, const char *subdir,
++                              const char *path, file_entry **dir)
++{
++    char *buf;
++
++    if (*path == '/' || *path == '\\')
++        path++;
++
++    if (subdir == NULL) {
++        setUnixDir(xdg_path, dir);
++        return path;
++    }
++
++    if (asprintf(&buf, "%s/%s", xdg_path, subdir) == -1)
++        return NULL;
++
++    setUnixDir(buf, dir);
++    free(buf);
++    return path;
++}
++
++#define MANGLE_PATH(xdgpath, subdir, off) \
++    origdosname = manglePath(xdgpath, subdir, origdosname + off, &parse_dir)
++
++static void maybeCreateSetupIni()
++{
++    char *buf;
++    int fd_in, fd_out;
++    static int done = 0;
++
++    if (done) return;
++
++    if (asprintf(&buf, "%s/%s", getConfigDir(), "setup.ini") == -1) {
++        return;
++    }
++
++    if (access(buf, F_OK) == 0) {
++        done = 1;
++        free(buf);
++        return;
++    }
++
++    makeDirs(getConfigDir());
++
++    fd_out = open(buf, O_WRONLY | O_CREAT, 0666);
++    free(buf);
++
++    if (fd_out == -1)
++        return;
++
++    if ((fd_in = open("@SETUP_INI_PATH@", O_RDONLY)) == -1) {
++        close(fd_out);
++        return;
++    }
++
++    buf = malloc(8192);
++
++    while (1) {
++        ssize_t result = read(fd_in, buf, 8192);
++        if (result == -1 || result == 0) break;
++        if (write(fd_out, buf, result) != result) break;
++    }
++
++    close(fd_in);
++    close(fd_out);
++    done = 1;
++}
++
+ /*
+ return value:
+ 0 - dos path found (realdir = found entry)
+@@ -292,9 +465,20 @@ return value:
+ int vfs_get_real_name(const char *origdosname, char *buf, file_entry **realdir)
+ {
+     char upperdosname[MAX_PATH], *dosname, *backslash;
+-    file_entry *parse_dir, *new_parse_dir;
++    file_entry *parse_dir = NULL, *new_parse_dir;
+     int ret;
+ 
++    if (strncasecmp(origdosname, "xldlibs\\current", 15) == 0) {
++        MANGLE_PATH(getDataDir(), "chars", 15);
++    } else if (strncasecmp(origdosname, "saves", 5) == 0) {
++        MANGLE_PATH(getDataDir(), "saves", 5);
++    } else if (strncasecmp(origdosname, "setup.ini", 10) == 0) {
++        maybeCreateSetupIni();
++        MANGLE_PATH(getConfigDir(), NULL, 0);
++    } else if (strncasecmp(origdosname, "setup.tmp", 10) == 0) {
++        MANGLE_PATH(getConfigDir(), NULL, 0);
++    }
++
+     // convert dos name to uppercase
+     {
+         int i;
+@@ -316,28 +500,30 @@ int vfs_get_real_name(const char *origdosname, char *buf, file_entry **realdir)
+ 
+ 
+     // find initial directory for parsing
+-    if (dosname[0] == '\\')
+-    {
+-        parse_dir = &Game_CDir;
+-        dosname++;
+-    }
+-    else if (dosname[0] == 'C' && dosname[1] == ':')
+-    {
+-        if (dosname[2] == '\\')
++    if (parse_dir == NULL) {
++        if (dosname[0] == '\\')
+         {
+             parse_dir = &Game_CDir;
+-            dosname+=3;
++            dosname++;
++        }
++        else if (dosname[0] == 'C' && dosname[1] == ':')
++        {
++            if (dosname[2] == '\\')
++            {
++                parse_dir = &Game_CDir;
++                dosname+=3;
++            }
++            else
++            {
++                parse_dir = Game_Current_Dir;
++                dosname+=2;
++            }
+         }
+         else
+         {
+             parse_dir = Game_Current_Dir;
+-            dosname+=2;
+         }
+     }
+-    else
+-    {
+-        parse_dir = Game_Current_Dir;
+-    }
+ 
+     // find directory
+     for (backslash = strchr(dosname, '\\'); backslash != NULL; backslash = strchr(dosname, '\\'))
diff --git a/pkgs/games/gog/crosscode.nix b/pkgs/games/gog/crosscode.nix
new file mode 100644
index 00000000..f33cdb45
--- /dev/null
+++ b/pkgs/games/gog/crosscode.nix
@@ -0,0 +1,32 @@
+{ lib, buildGame, fetchGog, makeWrapper, nwjs }:
+
+buildGame rec {
+  name = "crosscode-${version}";
+  version = "1.1.0";
+
+  src = fetchGog {
+    productId = 1252295864;
+    downloadName = "en3installer0";
+    sha256 = "1rqf1vlg151hxy5f9nwldmb4l3853dmvcf7fiakab8vzsmjmldlm";
+  };
+
+  nativeBuildInputs = [ makeWrapper ];
+
+  buildPhase = ''
+    substituteInPlace package.json --replace assets/ ""
+
+    # Remove Greenworks (Steamworks integration)
+    rm -r assets/modules
+  '';
+
+  installPhase = ''
+    mkdir -p "$out/share" "$out/bin"
+    cp -r assets "$out/share/crosscode"
+    install -vD -m 0644 package.json "$out/share/crosscode/package.json"
+
+    makeWrapper ${lib.escapeShellArg "${nwjs}/bin/nw"} "$out/bin/crosscode" \
+      --run "cd '$out/share/crosscode'" --add-flags .
+  '';
+
+  sandbox.paths.required = [ "$XDG_CONFIG_HOME/CrossCode" ];
+}
diff --git a/pkgs/games/gog/default.nix b/pkgs/games/gog/default.nix
new file mode 100644
index 00000000..aa672b4f
--- /dev/null
+++ b/pkgs/games/gog/default.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.gog;
+
+  self = rec {
+    callPackage = pkgs.lib.callPackageWith (pkgs // self);
+    callPackage_i686 = pkgs.lib.callPackageWith (pkgs.pkgsi686Linux // self);
+
+    fetchGog = callPackage ./fetch-gog {
+      inherit (config.gog) email password;
+    };
+
+    albion = callPackage_i686 ./albion {};
+    crosscode = callPackage ./crosscode.nix {};
+    dungeons3 = callPackage ./dungeons3.nix {};
+    epistory = callPackage ./epistory.nix { };
+    homm3 = callPackage ./homm3 {};
+    kingdoms-and-castles = callPackage ./kingdoms-and-castles.nix {};
+    overload = callPackage ./overload.nix {};
+    party-hard = callPackage ./party-hard.nix {};
+    planescape-torment-enhanced-edition = callPackage ./planescape-torment-enhanced-edition.nix {};
+    satellite-reign = callPackage ./satellite-reign.nix {};
+    settlers2 = callPackage ./settlers2.nix {};
+    stardew-valley = callPackage ./stardew-valley.nix {};
+    the-longest-journey = callPackage ./the-longest-journey {};
+    thimbleweed-park = callPackage ./thimbleweed-park.nix {};
+    war-for-the-overworld = callPackage ./war-for-the-overworld.nix {};
+    warcraft2 = callPackage ./warcraft2 {};
+    wizard-of-legend = callPackage ./wizard-of-legend.nix {};
+    xeen = callPackage ./xeen.nix {};
+  };
+in {
+  options.gog = {
+    email = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      default = null;
+      description = ''
+        Email address for your GOG account.
+      '';
+    };
+
+    password = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      default = null;
+      description = ''
+        Password for your GOG account.
+
+        <note><para>This will end up in the Nix store and other users on the
+        same machine can read it!</para></note>
+      '';
+    };
+  };
+
+  config.packages = {
+    gog = lib.mkIf (cfg.email != null && cfg.password != null) self;
+  };
+}
diff --git a/pkgs/games/gog/dungeons3.nix b/pkgs/games/gog/dungeons3.nix
new file mode 100644
index 00000000..53892f71
--- /dev/null
+++ b/pkgs/games/gog/dungeons3.nix
@@ -0,0 +1,16 @@
+{ buildUnity, fetchGog, mono }:
+
+buildUnity {
+  name = "dungeons3";
+  fullName = "Dungeons3";
+  saveDir = "Realmforge Studios GmbH/Dungeons 3";
+  version = "1.4.4";
+
+  src = fetchGog {
+    productId = 1346232158;
+    downloadName = "en3installer0";
+    sha256 = "1m4dvb91nfwxbgb76n8saznaaif053vb5wkdllb7imdbqqwlfsmy";
+  };
+
+  buildInputs = [ mono ];
+}
diff --git a/pkgs/games/gog/epistory.nix b/pkgs/games/gog/epistory.nix
new file mode 100644
index 00000000..8e9815c9
--- /dev/null
+++ b/pkgs/games/gog/epistory.nix
@@ -0,0 +1,35 @@
+{ buildUnity, fetchGog, fixFmodHook }:
+
+buildUnity rec {
+  name = "epistory";
+  fullName = "Epistory";
+  saveDir = "Fishing Cactus/Epistory";
+  version = "1.4-gog0";
+
+  src = fetchGog {
+    productId = 1986504189;
+    downloadName = "en3installer0";
+    sha256 = "05v9i4d7h2id5w6mfpnz3ig62v5dqibl74vahx3gqw9ya4jpgwv8";
+  };
+
+  buildInputs = [ fixFmodHook ];
+
+  meta = {
+    homepage = [
+      https://www.gog.com/game/epistory_typing_chronicles
+      http://epistorygame.com
+    ];
+    downloadPage = https://embed.gog.com/account/gameDetails/1986504189.json;
+    description = "a beautiful atmospheric 3D action/adventure typing game";
+    longDescription = ''
+      Epistory - Typing Chronicles is a beautiful atmospheric 3D
+      action/adventure typing game that tells the story of a writer lacking
+      inspiration who asks her muse to help write her latest book.
+
+      It features a visually stunning papercraft art style and blends light RPG
+      elements with exploration and unique combat mechanics solely using the
+      keyboard.
+    '';
+  };
+
+}
diff --git a/pkgs/games/gog/fetch-gog/default.nix b/pkgs/games/gog/fetch-gog/default.nix
new file mode 100644
index 00000000..c1d350ce
--- /dev/null
+++ b/pkgs/games/gog/fetch-gog/default.nix
@@ -0,0 +1,316 @@
+{ stdenv, lib, curl, writeText, runCommandCC, python3Packages, cacert
+, pkgconfig, qt5
+
+, email, password
+}:
+
+{ productId, downloadName, sha256, downloadType ? "installer", suffix ? "sh"
+, name ? "${toString productId}-${downloadName}-${downloadType}.${suffix}"
+}:
+
+let
+  # Taken from lgogdownloader (https://github.com/Sude-/lgogdownloader):
+  clientId = "46899977096215655";
+  clientSecret = "9d85c43b1482497dbbce61f6e4aa173a"
+               + "433796eeae2ca8c5f6129f2dc4de46d9";
+  redirectUri = "https://embed.gog.com/on_login_success?origin=client";
+
+  urlencode = url: query: let
+    urlquote = isQstring: val: let
+      extraSafeChars = lib.optionalString (!isQstring) "/:";
+      safeChars = lib.lowerChars ++ lib.upperChars
+               ++ lib.stringToCharacters ("0123456789_.-" + extraSafeChars);
+      charList = lib.stringToCharacters val;
+      hexify = chr: "%${import ./hexify-char.nix chr}";
+      quoteChar = chr: if lib.elem chr safeChars then chr else hexify chr;
+    in lib.concatMapStrings quoteChar charList;
+    mkKeyVal = key: val: "${urlquote true key}=${urlquote true val}";
+    qstring = lib.concatStringsSep "&" (lib.mapAttrsToList mkKeyVal query);
+  in urlquote false url + lib.optionalString (query != {}) "?${qstring}";
+
+  authURL = urlencode "https://auth.gog.com/auth" {
+    client_id = clientId;
+    redirect_uri = redirectUri;
+    response_type = "code";
+    layout = "default";
+    brand = "gog";
+  };
+
+  getCaptcha = let
+    mkCString = val: let
+      escaped = lib.replaceStrings ["\"" "\\" "\n"] ["\\\"" "\\\\" "\\n"] val;
+    in "\"${escaped}\"";
+
+    injectedJS = ''
+      var user_input = document.getElementById('login_username');
+      var pass_input = document.getElementById('login_password');
+      var submit_button = document.getElementById('login_login');
+
+      user_input.value = ${builtins.toJSON email};
+      user_input.style.display = 'none';
+      pass_input.value = ${builtins.toJSON password};
+      pass_input.style.display = 'none';
+
+      submit_button.style.display = 'none';
+
+      function waitForResponse() {
+        var response = grecaptcha.getResponse();
+        if (response != "")
+          submit_button.click();
+        else
+          setTimeout(waitForResponse, 50);
+      }
+
+      waitForResponse();
+    '';
+
+    application = writeText "captcha.cc" ''
+      #include <QApplication>
+      #include <QWebEngineView>
+      #include <QTcpServer>
+      #include <QQuickWebEngineProfile>
+      #include <QUrlQuery>
+
+      static QString clientId = ${mkCString clientId};
+      static QString clientSecret = ${mkCString clientSecret};
+      static QString redirectUri = ${mkCString redirectUri};
+
+      static QUrl getAuthUrl() {
+        QUrl url("https://auth.gog.com/auth");
+
+        QUrlQuery query;
+        query.addQueryItem("client_id", clientId);
+        query.addQueryItem("redirect_uri", redirectUri);
+        query.addQueryItem("response_type", "code");
+        query.addQueryItem("layout", "default");
+        query.addQueryItem("brand", "gog");
+
+        url.setQuery(query);
+        return url;
+      }
+
+      static QUrl getTokenUrl(const QString &code) {
+        QUrl url("https://auth.gog.com/token");
+
+        QUrlQuery query;
+        query.addQueryItem("client_id", clientId);
+        query.addQueryItem("client_secret", clientSecret);
+        query.addQueryItem("grant_type", "authorization_code");
+        query.addQueryItem("code", code);
+        query.addQueryItem("redirect_uri", redirectUri);
+
+        url.setQuery(query);
+        return url;
+      }
+
+      int main(int argc, char **argv) {
+        QApplication *app = new QApplication(argc, argv);
+        QTcpServer *server = new QTcpServer();
+        QWebEngineView *browser = new QWebEngineView();
+
+        QQuickWebEngineProfile::defaultProfile()->setOffTheRecord(true);
+
+        if (!server->listen(QHostAddress::LocalHost, 18321)) {
+          qCritical() << "Unable to listen on port 18321!";
+          return 1;
+        }
+
+        qInfo() << "Waiting for connection from the GOG login helper...";
+        if (!server->waitForNewConnection(-1)) {
+          qCritical() << "Unable to accept the connection!";
+          return 1;
+        }
+        qInfo() << "Connection established, spawning window to login.";
+
+        QTcpSocket *sock = server->nextPendingConnection();
+
+        browser->load(getAuthUrl());
+        browser->show();
+
+        browser->connect(browser, &QWebEngineView::loadFinished, [=]() {
+          browser->page()->runJavaScript(${mkCString injectedJS});
+          browser->connect(
+            browser, &QWebEngineView::urlChanged, [=](const QUrl &newurl
+          ) {
+            QUrlQuery newquery(newurl.query());
+            QString code = newquery.queryItemValue("code");
+            sock->write(getTokenUrl(code).toEncoded());
+            sock->flush();
+            sock->waitForBytesWritten();
+            sock->close();
+            server->close();
+            app->quit();
+          });
+        });
+
+        return app->exec();
+      }
+    '';
+
+  in stdenv.mkDerivation {
+    name = "get-captcha";
+
+    dontUnpack = true;
+
+    nativeBuildInputs = [ pkgconfig (qt5.wrapQtAppsHook or null) ];
+    buildInputs = [ qt5.qtbase qt5.qtwebengine ];
+    preferLocalBuild = true;
+
+    buildPhase = ''
+      g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
+        -Wall -std=c++11 -o get-captcha ${application}
+    '';
+
+    installPhase = ''
+      install -vD get-captcha "$out/bin/get-captcha"
+    '';
+  };
+
+  mkPyStr = str: "'${stdenv.lib.escape ["'" "\\"] (toString str)}'";
+
+  fetcher = writeText "fetch-gog.py" ''
+    import sys, socket, time
+    from urllib.request import urlopen, Request
+    from urllib.parse import urlsplit, urlencode, parse_qs
+    from urllib.error import HTTPError
+    from json import loads
+
+    import mechanicalsoup
+    from tabulate import tabulate
+
+    class GogFetcher:
+      def __init__(self, product_id, download_type, download_name):
+        self.product_id = product_id
+        self.download_type = download_type
+        self.download_name = download_name
+        self.login()
+
+      def login(self):
+        browser = mechanicalsoup.StatefulBrowser()
+        response = browser.open(${mkPyStr authURL})
+        if "https://www.recaptcha.net/recaptcha" in response.text:
+          token_url = self.login_with_captcha()
+        else:
+          browser.select_form('form[name="login"]')
+          browser['login[username]'] = ${mkPyStr email}
+          browser['login[password]'] = ${mkPyStr password}
+          browser.submit_selected()
+
+          query = parse_qs(urlsplit(browser.get_url()).query)
+
+          if 'code' not in query:
+            sys.stderr.write(
+              "Unable to login with the provided GOG credentials.\n"
+            )
+            raise SystemExit(1)
+
+          token_url = "https://auth.gog.com/token?" + urlencode({
+            'client_id': ${mkPyStr clientId},
+            'client_secret': ${mkPyStr clientSecret},
+            'grant_type': 'authorization_code',
+            'code': query['code'],
+            'redirect_uri': ${mkPyStr redirectUri}
+          })
+
+        response = urlopen(
+          token_url, cafile=${mkPyStr "${cacert}/etc/ssl/certs/ca-bundle.crt"}
+        )
+
+        self.access_token = loads(response.read())['access_token']
+
+      def login_with_captcha(self):
+        sys.stderr.write("Solving a captcha is required to log in.\n")
+        sys.stderr.write("Please run " ${mkPyStr getCaptcha}
+                         "/bin/get-captcha now.\n")
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sys.stderr.write("Waiting for connection")
+        i = 0
+        while sock.connect_ex(("127.0.0.1", 18321)) != 0:
+          time.sleep(0.1)
+          if i % 10 == 0:
+            sys.stderr.write('.')
+            sys.stderr.flush()
+          i += 1
+        sys.stderr.write(" connected.\n")
+        sys.stderr.write("Waiting for captcha to be solved...\n")
+        token_url = sock.recv(4096)
+        sock.close()
+        sys.stderr.write("Captcha solved correctly, logging in.\n")
+        return token_url.decode()
+
+      def request(self, url):
+        headers = {"Authorization": "Bearer " + self.access_token}
+        return loads(urlopen(
+          Request(url, headers=headers),
+          cafile=${mkPyStr "${cacert}/etc/ssl/certs/ca-bundle.crt"}
+        ).read())
+
+      def strip_dtype(self, dtype):
+        if dtype.endswith('es'):
+          return dtype[:-2]
+        elif dtype.endswith('s'):
+          return dtype[:-1]
+        else:
+          return dtype
+
+      def list_downloads(self):
+        url = "https://api.gog.com/products/" + self.product_id \
+            + "?expand=downloads,expanded_dlcs,related_products"
+        downloads = self.request(url)['downloads']
+        table = []
+        for dtype, dloads in downloads.items():
+          for dload in dloads:
+            for dlfile in dload['files']:
+              table.append([
+                self.strip_dtype(dtype), dload.get('os', ""),
+                dload.get('language_full', ""),
+                dlfile['id'], dlfile['size'], dlfile['downlink']
+              ])
+        sys.stderr.write(tabulate(table, headers=[
+          'Download type',
+          'Operating system',
+          'Language',
+          'Identifier',
+          'Size',
+          'URL'
+        ]) + "\n")
+
+      def fetch(self):
+        url = "http://api.gog.com/products/" + self.product_id \
+            + "/downlink/" + self.download_type + "/" + self.download_name
+        try:
+          download = self.request(url)
+        except HTTPError:
+          m = "Download {!r} with type {!r} not found.\nValid downloads are:\n"
+          sys.stderr.write(m.format(self.download_name, self.download_type))
+          self.list_downloads()
+          raise SystemExit(1)
+        else:
+          print(download['downlink'])
+
+    GogFetcher(sys.argv[1], sys.argv[2], sys.argv[3]).fetch()
+  '';
+
+in stdenv.mkDerivation {
+  inherit name;
+  outputHashAlgo = "sha256";
+  outputHash = sha256;
+
+  preferLocalBuild = true;
+
+  nativeBuildInputs = [
+    curl python3Packages.tabulate python3Packages.MechanicalSoup
+  ];
+
+  buildCommand = ''
+    url="$(${python3Packages.python.interpreter} ${fetcher} \
+      ${toString productId} \
+      ${lib.escapeShellArg downloadType} \
+      ${lib.escapeShellArg downloadName})"
+    header "downloading $name from $url"
+    curl \
+      --cacert ${lib.escapeShellArg "${cacert}/etc/ssl/certs/ca-bundle.crt"} \
+      --fail --output "$out" "$url"
+    stopNest
+  '';
+}
diff --git a/pkgs/games/gog/fetch-gog/hexify-char.nix b/pkgs/games/gog/fetch-gog/hexify-char.nix
new file mode 100644
index 00000000..78ae9159
--- /dev/null
+++ b/pkgs/games/gog/fetch-gog/hexify-char.nix
@@ -0,0 +1,258 @@
+val:
+
+if val == "" then "01"
+else if val == "" then "02"
+else if val == "" then "03"
+else if val == "" then "04"
+else if val == "" then "05"
+else if val == "" then "06"
+else if val == "" then "07"
+else if val == "" then "08"
+else if val == "\t" then "09"
+else if val == "\n" then "0a"
+else if val == "" then "0b"
+else if val == "" then "0c"
+else if val == "\r" then "0d"
+else if val == "" then "0e"
+else if val == "" then "0f"
+else if val == "" then "10"
+else if val == "" then "11"
+else if val == "" then "12"
+else if val == "" then "13"
+else if val == "" then "14"
+else if val == "" then "15"
+else if val == "" then "16"
+else if val == "" then "17"
+else if val == "" then "18"
+else if val == "" then "19"
+else if val == "" then "1a"
+else if val == "" then "1b"
+else if val == "" then "1c"
+else if val == "" then "1d"
+else if val == "" then "1e"
+else if val == "" then "1f"
+else if val == " " then "20"
+else if val == "!" then "21"
+else if val == "\"" then "22"
+else if val == "#" then "23"
+else if val == "$" then "24"
+else if val == "%" then "25"
+else if val == "&" then "26"
+else if val == "'" then "27"
+else if val == "(" then "28"
+else if val == ")" then "29"
+else if val == "*" then "2a"
+else if val == "+" then "2b"
+else if val == "," then "2c"
+else if val == "-" then "2d"
+else if val == "." then "2e"
+else if val == "/" then "2f"
+else if val == "0" then "30"
+else if val == "1" then "31"
+else if val == "2" then "32"
+else if val == "3" then "33"
+else if val == "4" then "34"
+else if val == "5" then "35"
+else if val == "6" then "36"
+else if val == "7" then "37"
+else if val == "8" then "38"
+else if val == "9" then "39"
+else if val == ":" then "3a"
+else if val == ";" then "3b"
+else if val == "<" then "3c"
+else if val == "=" then "3d"
+else if val == ">" then "3e"
+else if val == "?" then "3f"
+else if val == "@" then "40"
+else if val == "A" then "41"
+else if val == "B" then "42"
+else if val == "C" then "43"
+else if val == "D" then "44"
+else if val == "E" then "45"
+else if val == "F" then "46"
+else if val == "G" then "47"
+else if val == "H" then "48"
+else if val == "I" then "49"
+else if val == "J" then "4a"
+else if val == "K" then "4b"
+else if val == "L" then "4c"
+else if val == "M" then "4d"
+else if val == "N" then "4e"
+else if val == "O" then "4f"
+else if val == "P" then "50"
+else if val == "Q" then "51"
+else if val == "R" then "52"
+else if val == "S" then "53"
+else if val == "T" then "54"
+else if val == "U" then "55"
+else if val == "V" then "56"
+else if val == "W" then "57"
+else if val == "X" then "58"
+else if val == "Y" then "59"
+else if val == "Z" then "5a"
+else if val == "[" then "5b"
+else if val == "\\" then "5c"
+else if val == "]" then "5d"
+else if val == "^" then "5e"
+else if val == "_" then "5f"
+else if val == "`" then "60"
+else if val == "a" then "61"
+else if val == "b" then "62"
+else if val == "c" then "63"
+else if val == "d" then "64"
+else if val == "e" then "65"
+else if val == "f" then "66"
+else if val == "g" then "67"
+else if val == "h" then "68"
+else if val == "i" then "69"
+else if val == "j" then "6a"
+else if val == "k" then "6b"
+else if val == "l" then "6c"
+else if val == "m" then "6d"
+else if val == "n" then "6e"
+else if val == "o" then "6f"
+else if val == "p" then "70"
+else if val == "q" then "71"
+else if val == "r" then "72"
+else if val == "s" then "73"
+else if val == "t" then "74"
+else if val == "u" then "75"
+else if val == "v" then "76"
+else if val == "w" then "77"
+else if val == "x" then "78"
+else if val == "y" then "79"
+else if val == "z" then "7a"
+else if val == "{" then "7b"
+else if val == "|" then "7c"
+else if val == "}" then "7d"
+else if val == "~" then "7e"
+else if val == "" then "7f"
+else if val == "€" then "80"
+else if val == "" then "81"
+else if val == "‚" then "82"
+else if val == "ƒ" then "83"
+else if val == "„" then "84"
+else if val == "…" then "85"
+else if val == "†" then "86"
+else if val == "‡" then "87"
+else if val == "ˆ" then "88"
+else if val == "‰" then "89"
+else if val == "Š" then "8a"
+else if val == "‹" then "8b"
+else if val == "Œ" then "8c"
+else if val == "" then "8d"
+else if val == "Ž" then "8e"
+else if val == "" then "8f"
+else if val == "" then "90"
+else if val == "‘" then "91"
+else if val == "’" then "92"
+else if val == "“" then "93"
+else if val == "”" then "94"
+else if val == "•" then "95"
+else if val == "–" then "96"
+else if val == "—" then "97"
+else if val == "˜" then "98"
+else if val == "™" then "99"
+else if val == "š" then "9a"
+else if val == "›" then "9b"
+else if val == "œ" then "9c"
+else if val == "" then "9d"
+else if val == "ž" then "9e"
+else if val == "Ÿ" then "9f"
+else if val == " " then "a0"
+else if val == "¡" then "a1"
+else if val == "¢" then "a2"
+else if val == "£" then "a3"
+else if val == "¤" then "a4"
+else if val == "¥" then "a5"
+else if val == "¦" then "a6"
+else if val == "§" then "a7"
+else if val == "¨" then "a8"
+else if val == "©" then "a9"
+else if val == "ª" then "aa"
+else if val == "«" then "ab"
+else if val == "¬" then "ac"
+else if val == "­" then "ad"
+else if val == "®" then "ae"
+else if val == "¯" then "af"
+else if val == "°" then "b0"
+else if val == "±" then "b1"
+else if val == "²" then "b2"
+else if val == "³" then "b3"
+else if val == "´" then "b4"
+else if val == "µ" then "b5"
+else if val == "¶" then "b6"
+else if val == "·" then "b7"
+else if val == "¸" then "b8"
+else if val == "¹" then "b9"
+else if val == "º" then "ba"
+else if val == "»" then "bb"
+else if val == "¼" then "bc"
+else if val == "½" then "bd"
+else if val == "¾" then "be"
+else if val == "¿" then "bf"
+else if val == "À" then "c0"
+else if val == "Á" then "c1"
+else if val == "Â" then "c2"
+else if val == "Ã" then "c3"
+else if val == "Ä" then "c4"
+else if val == "Å" then "c5"
+else if val == "Æ" then "c6"
+else if val == "Ç" then "c7"
+else if val == "È" then "c8"
+else if val == "É" then "c9"
+else if val == "Ê" then "ca"
+else if val == "Ë" then "cb"
+else if val == "Ì" then "cc"
+else if val == "Í" then "cd"
+else if val == "Î" then "ce"
+else if val == "Ï" then "cf"
+else if val == "Ð" then "d0"
+else if val == "Ñ" then "d1"
+else if val == "Ò" then "d2"
+else if val == "Ó" then "d3"
+else if val == "Ô" then "d4"
+else if val == "Õ" then "d5"
+else if val == "Ö" then "d6"
+else if val == "×" then "d7"
+else if val == "Ø" then "d8"
+else if val == "Ù" then "d9"
+else if val == "Ú" then "da"
+else if val == "Û" then "db"
+else if val == "Ü" then "dc"
+else if val == "Ý" then "dd"
+else if val == "Þ" then "de"
+else if val == "ß" then "df"
+else if val == "à" then "e0"
+else if val == "á" then "e1"
+else if val == "â" then "e2"
+else if val == "ã" then "e3"
+else if val == "ä" then "e4"
+else if val == "å" then "e5"
+else if val == "æ" then "e6"
+else if val == "ç" then "e7"
+else if val == "è" then "e8"
+else if val == "é" then "e9"
+else if val == "ê" then "ea"
+else if val == "ë" then "eb"
+else if val == "ì" then "ec"
+else if val == "í" then "ed"
+else if val == "î" then "ee"
+else if val == "ï" then "ef"
+else if val == "ð" then "f0"
+else if val == "ñ" then "f1"
+else if val == "ò" then "f2"
+else if val == "ó" then "f3"
+else if val == "ô" then "f4"
+else if val == "õ" then "f5"
+else if val == "ö" then "f6"
+else if val == "÷" then "f7"
+else if val == "ø" then "f8"
+else if val == "ù" then "f9"
+else if val == "ú" then "fa"
+else if val == "û" then "fb"
+else if val == "ü" then "fc"
+else if val == "ý" then "fd"
+else if val == "þ" then "fe"
+else if val == "ÿ" then "ff"
+else throw "Invalid character '${val}'."
diff --git a/pkgs/games/gog/homm3/default.nix b/pkgs/games/gog/homm3/default.nix
new file mode 100644
index 00000000..5aecb8f2
--- /dev/null
+++ b/pkgs/games/gog/homm3/default.nix
@@ -0,0 +1,97 @@
+{ stdenv, lib, buildSandbox, fetchGog, runCommand, makeWrapper, fetchFromGitHub
+, cmake, pkgconfig, python3, boost, zlib, minizip, qt5
+, SDL2, SDL2_image, SDL2_mixer, SDL2_ttf
+, innoextract, parallel, ffmpeg
+}:
+
+let
+  data = runCommand "homm3-complete-data" rec {
+    version = "4.0";
+
+    # We need a newer version that 1.7, because GOG uses a newer archive
+    # format.
+    nativeBuildInputs = lib.singleton (innoextract.overrideAttrs (drv: {
+      src = fetchFromGitHub {
+        owner = "dscharrer";
+        repo = "innoextract";
+        rev = "4c61bc4da822fc89f2e05bdb2c45e6c4dd7a3673";
+        sha256 = "197pr7dzlza4isssvhqhvnrr7wzc9c4b3wnnp03sxpmhviyidln1";
+      };
+    })) ++ [ parallel ffmpeg ];
+
+    data = fetchGog {
+      name = "setup_homm_3_complete_${version}.bin";
+      productId = 1207658787;
+      downloadName = "en1installer1";
+      sha256 = "1wfly3024yi64kaczfdca4wx5g09053dpc1gwp08w637833n4kq4";
+    };
+
+    setup = fetchGog {
+      name = "setup_homm_3_complete_${version}.exe";
+      productId = 1207658787;
+      downloadName = "en1installer0";
+      sha256 = "1cwr28ml9z3iq6q9z1vs1jkbnjjrkv2m39bhqw78a5hvj43mgxza";
+    };
+  } ''
+    ln -s "$data" archive-1.bin
+    ln -s "$setup" archive.exe
+    innoextract -L -I Data -I Maps -I Mp3 archive.exe
+    mkdir -p "$out/music"
+    parallel -v ffmpeg -hide_banner -loglevel warning -i {} -acodec libvorbis \
+      "$out/music/{/.}.ogg" ::: mp3/*.mp3
+    mv -t "$out" data maps
+  '';
+
+  engine = stdenv.mkDerivation rec {
+    name = "vcmi-${version}";
+    version = "20190609";
+
+    src = fetchFromGitHub {
+      owner = "vcmi";
+      repo = "vcmi";
+      rev = "e7bced112cf36007da8f418ba3313d2dd4b3e045";
+      sha256 = "0qk0mpz3amg2kw5m99bk3qi19rwcwjj6s1lclby1ws0v8nxh2cmb";
+      fetchSubmodules = true;
+    };
+
+    inherit data;
+
+    patches = [ ./launcher-execl.patch ];
+
+    postPatch = ''
+      find -type f -name '*.cpp' -exec sed -i -e '/^ *# *include/ {
+        s!["<]SDL_\(ttf\|image\|mixer\)\.h[">]!<SDL2/SDL_\1.h>!
+      }' {} +
+
+      sed -i -e 's/"Mp3"/"music"/' config/filesystem.json
+    '';
+
+    cmakeFlags = [ "-DCMAKE_INSTALL_LIBDIR=lib" "-DENABLE_TEST=0" ];
+    enableParallelBuilding = true;
+    nativeBuildInputs = [ cmake pkgconfig python3 makeWrapper ];
+    buildInputs = [
+      boost zlib minizip SDL2 SDL2_image SDL2_mixer SDL2_ttf ffmpeg
+      qt5.qtbase
+    ];
+    postInstall = let
+      inherit (qt5.qtbase) qtPluginPrefix;
+      qtPlugins = "${qt5.qtbase}/${qtPluginPrefix}";
+    in ''
+      rm "$out/bin/vcmibuilder"
+      for i in "$out/bin/"*; do
+        rpath="$(patchelf --print-rpath "$i")"
+        patchelf --set-rpath "$out/lib/vcmi:$rpath" "$i"
+      done
+
+      wrapProgram "$out/bin/vcmilauncher" \
+        --suffix QT_PLUGIN_PATH : ${lib.escapeShellArg qtPlugins}
+      cp -rst "$out/share/vcmi" "$data"/*
+    '';
+    dontStrip = true;
+  };
+
+in buildSandbox engine {
+  allowBinSh = true;
+  paths.required = [ "$XDG_DATA_HOME/vcmi" "$XDG_CONFIG_HOME/vcmi" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" "LOCALE_ARCHIVE" ];
+}
diff --git a/pkgs/games/gog/homm3/launcher-execl.patch b/pkgs/games/gog/homm3/launcher-execl.patch
new file mode 100644
index 00000000..fae0fa80
--- /dev/null
+++ b/pkgs/games/gog/homm3/launcher-execl.patch
@@ -0,0 +1,36 @@
+diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp
+index a07774ed2..3275af71a 100644
+--- a/launcher/mainwindow_moc.cpp
++++ b/launcher/mainwindow_moc.cpp
+@@ -11,7 +11,7 @@
+ #include "mainwindow_moc.h"
+ #include "ui_mainwindow_moc.h"
+ 
+-#include <QProcess>
++#include <unistd.h>
+ #include <QDir>
+ 
+ #include "../lib/CConfigHandler.h"
+@@ -77,19 +77,11 @@ void MainWindow::on_startGameButton_clicked()
+ 
+ void MainWindow::startExecutable(QString name)
+ {
+-	QProcess process;
+-
+-	// Start the executable
+-	if(process.startDetached(name))
+-	{
+-		close(); // exit launcher
+-	}
+-	else
+-	{
++	if (execl(name.toLatin1().data(), "vcmiclient", nullptr) == -1) {
++		QString msg("Failed to start %1\nReason: %2");
+ 		QMessageBox::critical(this,
+ 		                      "Error starting executable",
+-		                      "Failed to start " + name + "\n"
+-		                      "Reason: " + process.errorString(),
++							  msg.arg(name).arg(strerror(errno)),
+ 		                      QMessageBox::Ok,
+ 		                      QMessageBox::Ok);
+ 		return;
diff --git a/pkgs/games/gog/kingdoms-and-castles.nix b/pkgs/games/gog/kingdoms-and-castles.nix
new file mode 100644
index 00000000..e31551cc
--- /dev/null
+++ b/pkgs/games/gog/kingdoms-and-castles.nix
@@ -0,0 +1,14 @@
+{ buildUnity, fetchGog }:
+
+buildUnity {
+  name = "kingdoms-and-castles";
+  fullName = "KingdomsAndCastles";
+  saveDir = "LionShield/Kingdoms and Castles";
+  version = "115r12";
+
+  src = fetchGog {
+    productId = 2067763543;
+    downloadName = "en3installer0";
+    sha256 = "1ag03piq09z7hljcbs145hyj8z0gjcvffj99znf3mnbw2qipb7pq";
+  };
+}
diff --git a/pkgs/games/gog/overload.nix b/pkgs/games/gog/overload.nix
new file mode 100644
index 00000000..f1807931
--- /dev/null
+++ b/pkgs/games/gog/overload.nix
@@ -0,0 +1,14 @@
+{ buildUnity, fetchGog }:
+
+buildUnity {
+  name = "overload";
+  fullName = "Overload";
+  saveDir = "Revival/Overload";
+  version = "1.0.1854";
+
+  src = fetchGog {
+    productId = 1309632191;
+    downloadName = "en3installer0";
+    sha256 = "1qyd78xzd39763dmrb5rb8g0v0qi45jjkb9id9gjvvmh41m0731i";
+  };
+}
diff --git a/pkgs/games/gog/party-hard.nix b/pkgs/games/gog/party-hard.nix
new file mode 100644
index 00000000..6b15580f
--- /dev/null
+++ b/pkgs/games/gog/party-hard.nix
@@ -0,0 +1,14 @@
+{ buildUnity, fetchGog }:
+
+buildUnity {
+  name = "party-hard";
+  fullName = "PartyHardGame";
+  saveDir = "PinoklGames/PartyHardGame";
+  version = "1.4.030r";
+
+  src = fetchGog {
+    productId = 1236689966;
+    downloadName = "en3installer2";
+    sha256 = "00sb76w0v4b2izfwx1kr53frrgg8rg5d0qgpj8z3xq8frnv1fmi4";
+  };
+}
diff --git a/pkgs/games/gog/planescape-torment-enhanced-edition.nix b/pkgs/games/gog/planescape-torment-enhanced-edition.nix
new file mode 100644
index 00000000..a26cc574
--- /dev/null
+++ b/pkgs/games/gog/planescape-torment-enhanced-edition.nix
@@ -0,0 +1,32 @@
+{ lib, buildGame, fetchGog, makeWrapper
+, openal, libGL, openssl_1_0_2, xorg, expat }:
+
+buildGame {
+  name = "planescape-torment";
+  fullName = "Planescape Torment: Enhanced Edition";
+  saveDir = "Beamdog/Planescape Torment Enhanced Edition";
+  version = "3.1.4";
+
+  src = fetchGog {
+    productId = 1132393016;
+    downloadName = "en3installer0";
+    sha256 = "1plil37525l20j1fpk8726v6vh8rsny2x06msvd2q0900j8xlbl1";
+  };
+
+  nativeBuildInputs = [ makeWrapper ];
+  buildInputs = [ openal libGL openssl_1_0_2 xorg.libX11 expat ];
+
+  installPhase = ''
+    SHARE=$out/share/planescape-torment
+    mkdir -p $SHARE
+    mv ./* $SHARE
+    rm $SHARE/Torment
+    mkdir $out/bin
+    mv $SHARE/Torment64 $out/bin/planescape-torment
+    chmod +x $out/bin/planescape-torment
+    wrapProgram $out/bin/planescape-torment \
+      --run "cd '$SHARE'"
+  '';
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/Planescape Torment - Enhanced Edition" ];
+}
diff --git a/pkgs/games/gog/satellite-reign.nix b/pkgs/games/gog/satellite-reign.nix
new file mode 100644
index 00000000..63f0c8b5
--- /dev/null
+++ b/pkgs/games/gog/satellite-reign.nix
@@ -0,0 +1,16 @@
+{ buildUnity, fetchGog }:
+
+buildUnity {
+  name = "satellite-reign";
+  fullName = "SatelliteReignLinux";
+  saveDir = "5 Lives Studios/SatelliteReign";
+  version = "1.13.06";
+
+  src = fetchGog {
+    productId = 1428054996;
+    downloadName = "en3installer9";
+    sha256 = "0wpkpqrcli2772g6l9yab38vbjh1by4cbpa397fqvhny247qdz5k";
+  };
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/SatelliteReign" ];
+}
diff --git a/pkgs/games/gog/settlers2.nix b/pkgs/games/gog/settlers2.nix
new file mode 100644
index 00000000..4f0ac193
--- /dev/null
+++ b/pkgs/games/gog/settlers2.nix
@@ -0,0 +1,99 @@
+{ stdenv, lib, buildSandbox, fetchGog, gogUnpackHook, bchunk, p7zip
+
+, fetchFromGitHub, cmake, gettext, boost, miniupnpc, bzip2
+, SDL, SDL_mixer, libpulseaudio, alsaLib, libGL, lua5_2
+}:
+
+let
+  gameData = stdenv.mkDerivation rec {
+    name = "settlers2-game-data-${version}";
+    version = "1.31";
+
+    src = fetchGog {
+      productId = 1207658786;
+      downloadName = "en1installer0";
+      sha256 = "19c88h972ydfpdbay61lz6pi4gnlm2lq5dcya5im9mmlin2nvyr7";
+    };
+
+    nativeBuildInputs = [ gogUnpackHook ];
+    innoExtractOnly = [ "/app/DATA" "/app/GFX" ];
+    innoExtractKeepCase = true;
+
+    phases = [ "unpackPhase" "patchPhase" "installPhase" ];
+
+    installPhase = ''
+      mkdir -p "$out"
+      mv -t "$out" DATA GFX
+    '';
+  };
+
+in buildSandbox (stdenv.mkDerivation rec {
+  name = "settlers2-${version}";
+  version = "20180702";
+
+  src = fetchFromGitHub {
+    repo = "s25client";
+    owner = "Return-To-The-Roots";
+    rev = "27721e58fbaedd2be9489d1926a2fc9d6387f372";
+    sha256 = "051saafh2scdi284gl16z2nqwxq71wnr6fsbs189wvm5w2ly2y9r";
+    fetchSubmodules = true;
+  };
+
+  postPatch = ''
+    # The build process tries to figure out the version from .git, so let's
+    # use the commit from the "src" attribute.
+    mkdir .git
+    echo ${lib.escapeShellArg src.rev} > .git/HEAD
+
+    # This tries to mix the LUA version in contrib with the one in nixpkgs.
+    rm -rf contrib/lua
+
+    # We already bake in the gameData store path, so there is no need to put a
+    # placeholder in there (which will fail anyway, because it can't write to
+    # gameData).
+    sed -i -e '/install.*RTTR_S2_PLACEHOLDER_PATH/d' CMakeLists.txt
+
+    # SOUND.LST is generated in postInstall, so let's correct the path.
+    substituteInPlace rttrConfig/files.h \
+      --replace '<RTTR_USERDATA>/LSTS/SOUND.LST' '<RTTR_RTTR>/LSTS/SOUND.LST'
+
+    # Use "$XDG_DATA_HOME/settlers2" instead of "$HOME/.s25rttr".
+    sed -i -e 's!getPathFromEnvVar("HOME")!${
+      "getPathFromEnvVar(\"XDG_DATA_HOME\");" +
+      "if (homePath.empty()) homePath = " +
+      "getPathFromEnvVar(\"HOME\") / \".local\" / \"share\""
+    }!' libutil/src/System.cpp
+    sed -i -e '1i #define RTTR_SETTINGSDIR "~/settlers2"' \
+      rttrConfig/RttrConfig.cpp
+  '';
+
+  cmakeFlags = [ "-DRTTR_GAMEDIR=${gameData}" ];
+
+  nativeBuildInputs = [ cmake gettext ];
+  buildInputs = [
+    boost miniupnpc SDL SDL_mixer bzip2 libpulseaudio alsaLib libGL lua5_2
+  ];
+
+  postInstall = ''
+    # Rename the game binaries to match up the derivation name.
+    mv "$out/bin/s25client" "$out/bin/settlers2"
+    mv "$out/bin/s25edit" "$out/bin/settlers2editor"
+
+    # We don't want the updater and store paths are immutable anyway.
+    rm "$out/bin/rttr.sh"
+
+    # Convert sounds from game data, which is usually done at runtime but we
+    # can avoid it because we already have the game data available.
+    "$out/libexec/s25rttr/sound-convert" \
+      -s "$out/share/s25rttr/RTTR/sound.scs" \
+      -f ${lib.escapeShellArg gameData}/DATA/SOUNDDAT/SOUND.LST \
+      -t "$out/share/s25rttr/RTTR/LSTS/SOUND.LST"
+
+    # The sound converter and resampler now are no longer needed.
+    rm "$out/libexec/s25rttr/sound-convert" "$out/libexec/s25rttr/s-c_resample"
+    rmdir "$out/libexec/s25rttr" "$out/libexec"
+  '';
+}) {
+  paths.required = [ "$XDG_DATA_HOME/settlers2" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" "LOCALE_ARCHIVE" ];
+}
diff --git a/pkgs/games/gog/stardew-valley.nix b/pkgs/games/gog/stardew-valley.nix
new file mode 100644
index 00000000..b66a2399
--- /dev/null
+++ b/pkgs/games/gog/stardew-valley.nix
@@ -0,0 +1,61 @@
+{ lib, stdenv, buildGame, fetchGog, makeWrapper
+, mono, SDL2, libGL, openal
+}:
+
+buildGame rec {
+  name = "stardew-valley-${version}";
+  version = "1.3.33";
+
+  src = fetchGog {
+    productId = 1453375253;
+    downloadName = "en3installer0";
+    sha256 = "1akqnawy2rzyxlkpjip6fa0isnzna131n09fr19i82qj9ywz2c1j";
+  };
+
+  nativeBuildInputs = [ makeWrapper ];
+
+  buildPhase = let
+    dllmap = {
+      SDL2 = "${SDL2}/lib/libSDL2.so";
+      soft_oal = "${openal}/lib/libopenal.so";
+    };
+  in lib.concatStrings (lib.mapAttrsToList (dll: target: ''
+    sed -i -e '/<dllmap.*dll="${dll}\.dll".*os="linux"/ {
+      s!target="[^"]*"!target="'"${target}"'"!
+    }' MonoGame.Framework.dll.config
+  '') dllmap) + ''
+    sed -i -e '/<dllmap.*os="linux"/ {
+      s!target="[^"]*"!target="${
+        "'\"$out\"'/libexec/stardew-valley/libGalaxyCSharpGlue.so"
+      }"!
+    }' GalaxyCSharp.dll.config
+  '';
+
+  bitSuffix = lib.optionalString stdenv.is64bit 64;
+
+  installPhase = ''
+    mkdir -p "$out/share" "$out/libexec/stardew-valley"
+
+    cp -rv Content "$out/share/stardew-valley"
+    cp -rv monoconfig "$out/libexec/stardew-valley/StardewValley.exe.config"
+    cp -rvt "$out/libexec/stardew-valley" StardewValley.exe \
+      MonoGame.Framework.dll* BmFont.dll xTile.dll Lidgren.Network.dll \
+      GalaxyCSharp.dll
+    ln -s "$out/share/stardew-valley" "$out/libexec/stardew-valley/Content"
+
+    install -vD "lib$bitSuffix/libGalaxy$bitSuffix.so" \
+      "$out/libexec/stardew-valley/libGalaxy$bitSuffix.so"
+    install -vD "lib$bitSuffix/libGalaxyCSharpGlue.so" \
+      "$out/libexec/stardew-valley/libGalaxyCSharpGlue.so"
+
+    makeWrapper ${lib.escapeShellArg mono}/bin/mono \
+      "$out/bin/stardew-valley" \
+      --add-flags "$out/libexec/stardew-valley/StardewValley.exe" \
+      --prefix LD_LIBRARY_PATH : ${lib.escapeShellArg "${libGL}/lib"} \
+      --run "cd '$out/libexec/stardew-valley'"
+  '';
+
+  sandbox.paths.required = [
+    "$XDG_DATA_HOME/StardewValley" "$XDG_CONFIG_HOME/StardewValley"
+  ];
+}
diff --git a/pkgs/games/gog/the-longest-journey/default.nix b/pkgs/games/gog/the-longest-journey/default.nix
new file mode 100644
index 00000000..9dca199a
--- /dev/null
+++ b/pkgs/games/gog/the-longest-journey/default.nix
@@ -0,0 +1,106 @@
+{ stdenv, lib, fetchGog, fetchFromGitHub, innoextract, runCommand, buildSandbox
+, SDL2, SDL2_net, freetype, libGLU_combined, glew, alsaLib
+, libogg, libvorbis, xvfb_run
+}:
+
+let
+  gameData = runCommand "the-longest-journey-data" rec {
+    version = "142";
+
+    # We need a newer version that 1.7, because GOG uses a newer archive
+    # format.
+    nativeBuildInputs = lib.singleton (innoextract.overrideAttrs (drv: {
+      src = fetchFromGitHub {
+        owner = "dscharrer";
+        repo = "innoextract";
+        rev = "4c61bc4da822fc89f2e05bdb2c45e6c4dd7a3673";
+        sha256 = "197pr7dzlza4isssvhqhvnrr7wzc9c4b3wnnp03sxpmhviyidln1";
+      };
+    }));
+
+    data = fetchGog {
+      name = "the-longest-journey-${version}.bin";
+      productId = 1207658794;
+      downloadName = "en1installer1";
+      sha256 = "08jg5snlxkzxppq37lsmbhgv9zhwnk1zr4cid5gynzq9b1048rzc";
+    };
+
+    setup = fetchGog {
+      name = "the-longest-journey-${version}.exe";
+      productId = 1207658794;
+      downloadName = "en1installer0";
+      sha256 = "1h4c2bhf5mhz004r37dwdydl3rhpg1wyr4kyvxxwma7x9grxqyzc";
+    };
+  } ''
+    ln -s "$data" archive-1.bin
+    ln -s "$setup" archive.exe
+    innoextract -L -m archive.exe
+    mkdir "$out"
+    mv -t "$out" \
+      game.exe gui.ini chapters.ini language.ini x.xarc \
+      static global fonts [a-f0-9][a-f0-9]
+  '';
+
+  residualvm = stdenv.mkDerivation rec {
+    name = "residualvm-${version}";
+    version = "20190611";
+
+    src = fetchFromGitHub {
+      owner = "residualvm";
+      repo = "residualvm";
+      rev = "ae1a7fbf6fa6bf88a7adebaedb2cd713d5ccc718";
+      sha256 = "1521578jis9s3ilz0ws0msanviyqf70dp54db3d6ssfikc0w3myx";
+    };
+
+    patches = [ ./predefined-config.patch ];
+
+    # Current Git version has an --enable-static option so the stdenv setup
+    # thinks that there is --disable-static as well, which doesn't exist.
+    dontDisableStatic = true;
+
+    enableParallelBuilding = true;
+    buildInputs = [
+      SDL2 SDL2_net freetype libGLU_combined glew alsaLib
+      libogg libvorbis
+    ];
+
+    configureFlags = [ "--disable-all-engines" "--enable-engine=stark" ];
+  };
+
+  configFile = runCommand "residualvm-stark.ini" {
+    nativeBuildInputs = [ xvfb_run residualvm ];
+    inherit gameData;
+  } ''
+    xvfb-run residualvm -p "$gameData" -a
+    sed -e '/^\[residualvm\]/a enable_unsupported_game_warning=false' \
+      residualvm.ini > "$out"
+  '';
+
+  unsandboxed = runCommand "the-longest-journey-${gameData.version}" {
+    residualCmd = "${residualvm}/bin/residualvm";
+    configArgs = let
+      mkXdg = what: fallback: extra: let
+        basePath = "\${XDG_${what}_HOME:-$HOME/${fallback}}";
+      in "\"${basePath}/the-longest-journey${extra}\"";
+    in [
+      "--savepath=${mkXdg "DATA" ".local/share" ""}"
+      "--config=${mkXdg "CONFIG" ".config" "/settings.ini"}"
+    ];
+    inherit (stdenv) shell;
+    residualArgs = lib.escapeShellArgs [ "--predefined-config=${configFile}" ];
+  } ''
+    mkdir -p "$out/bin"
+    cat > "$out/bin/the-longest-journey" <<EOF
+    #!$shell
+    exec $residualCmd $residualArgs $configArgs "\$@" tlj-win
+    EOF
+    chmod +x "$out/bin/the-longest-journey"
+  '';
+
+in buildSandbox unsandboxed {
+  paths.required = [
+    "$XDG_CONFIG_HOME/the-longest-journey"
+    "$XDG_DATA_HOME/the-longest-journey"
+  ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" ];
+}
diff --git a/pkgs/games/gog/the-longest-journey/predefined-config.patch b/pkgs/games/gog/the-longest-journey/predefined-config.patch
new file mode 100644
index 00000000..504e3867
--- /dev/null
+++ b/pkgs/games/gog/the-longest-journey/predefined-config.patch
@@ -0,0 +1,29 @@
+diff --git a/base/commandLine.cpp b/base/commandLine.cpp
+index ab741917..8723fc0d 100644
+--- a/base/commandLine.cpp
++++ b/base/commandLine.cpp
+@@ -425,6 +425,9 @@ Common::String parseCommandLine(Common::StringMap &settings, int argc, const cha
+ 			DO_LONG_COMMAND("list-saves")
+ 			END_COMMAND
+ 
++			DO_LONG_OPTION("predefined-config")
++			END_OPTION
++
+ 			DO_OPTION('c', "config")
+ 			END_OPTION
+ 
+diff --git a/base/main.cpp b/base/main.cpp
+index 2fbfc679..d74b15e5 100644
+--- a/base/main.cpp
++++ b/base/main.cpp
+@@ -394,6 +394,10 @@ extern "C" int scummvm_main(int argc, const char * const argv[]) {
+ 	Common::StringMap settings;
+ 	command = Base::parseCommandLine(settings, argc, argv);
+ 
++	// Load config file with predefined options
++	if (settings.contains("predefined-config"))
++		ConfMan.loadConfigFile(settings["predefined-config"]);
++
+ 	// Load the config file (possibly overridden via command line):
+ 	if (settings.contains("config")) {
+ 		ConfMan.loadConfigFile(settings["config"]);
diff --git a/pkgs/games/gog/thimbleweed-park.nix b/pkgs/games/gog/thimbleweed-park.nix
new file mode 100644
index 00000000..ec48233d
--- /dev/null
+++ b/pkgs/games/gog/thimbleweed-park.nix
@@ -0,0 +1,66 @@
+{ lib, buildGame, fetchGog, libGL, libudev
+
+, ransomeUnbeeped ? true
+}:
+
+buildGame rec {
+  name = "thimbleweed-park-${version}";
+  version = "1.0.958";
+
+  srcs = lib.singleton (fetchGog {
+    productId = 1325604411;
+    downloadName = "en3installer0";
+    sha256 = "141263g749k9h4741q8dyv7149n20zx50rhl0ny8ly6p4yw1spv7";
+  }) ++ lib.optional ransomeUnbeeped (fetchGog {
+    productId = 1858019230;
+    downloadName = "en3installer0";
+    sha256 = "1cfll73qazm9nz40n963qvankqkznfjai9g88kgw6xcl40y8jrqn";
+  });
+
+  buildInputs = [ libGL ];
+
+  runtimeDependencies = [ libudev ];
+
+  # A small preload wrapper which changes into the data directory during
+  # startup of the main binary.
+  buildPhase = ''
+    cc -Werror -Wall -std=c11 -shared -xc - -o preload.so -fPIC <<EOF
+    #include <stdio.h>
+    #include <unistd.h>
+
+    __attribute__((constructor)) static void chdirToData(void) {
+      if (chdir("$out/share/thimbleweed-park") == -1) {
+        perror("chdir $out/share/thimbleweed-park");
+        _exit(1);
+      }
+    }
+    EOF
+    patchelf --add-needed "$out/libexec/thimbleweed-park/preload.so" \
+      ThimbleweedPark
+  '';
+
+  installPhase = ''
+    install -vD ThimbleweedPark "$out/bin/thimbleweed-park"
+    install -vD preload.so "$out/libexec/thimbleweed-park/preload.so"
+    for i in *.ggpack[0-9]*; do
+      install -vD -m 0644 "$i" "$out/share/thimbleweed-park/$(basename "$i")"
+    done
+
+    install -vD -m 0644 xdg-icon.png "$out/share/icons/thimbleweed-park.png"
+
+    mkdir -p "$out/share/applications"
+    cat > "$out/share/applications/thimbleweed-park.desktop" <<EOF
+    [Desktop Entry]
+    Name=Thimbleweed Park
+    Type=Application
+    Version=1.1
+    Exec=$out/bin/thimbleweed-park
+    Icon=$out/share/icons/thimbleweed-park.png
+    Categories=Game
+    EOF
+  '';
+
+  sandbox.paths.required = [
+    "$XDG_DATA_HOME/Terrible Toybox/Thimbleweed Park"
+  ];
+}
diff --git a/pkgs/games/gog/war-for-the-overworld.nix b/pkgs/games/gog/war-for-the-overworld.nix
new file mode 100644
index 00000000..fb139fd8
--- /dev/null
+++ b/pkgs/games/gog/war-for-the-overworld.nix
@@ -0,0 +1,86 @@
+{ buildUnity, fetchGog, mono, monogamePatcher }:
+
+buildUnity {
+  name = "war-for-the-overworld";
+  fullName = "WFTOGame";
+  saveDir = "Subterranean Games/War For The Overworld";
+  version = "2.0.4";
+
+  src = fetchGog {
+    productId = 1964276929;
+    downloadName = "en3installer0";
+    sha256 = "0p54dhd2j7zvc78444jnjmkjv7kf6sskbphxkj5vlxmfcrmbd2xq";
+  };
+
+  nativeBuildInputs = [ mono monogamePatcher ];
+
+  # The game tries to write stuff to its dataPath and it's even more
+  # complicated than for most other games that try to write stuff into their
+  # dataPath because the paths overlap with the assets.
+  #
+  # I've reported this upstream at:
+  #
+  # https://brightrockgames.userecho.com/communities/1/topics/4720-xdg
+  #
+  # So let's patch a few stuff so that at least starting the game and
+  # loading/saving games will work.
+  buildPhase = ''
+    cat > nix-support.cs <<EOF
+    using UnityEngine;
+    using System.IO;
+
+    public class NixSupport {
+      public static string GetDataDir() {
+        var path = Path.Combine(Application.persistentDataPath, "GameData");
+        if (!Directory.Exists(path))
+          Directory.CreateDirectory(path);
+        return path;
+      }
+
+      public static string GetDataDir(string subpath) {
+        var path = Path.Combine(NixSupport.GetDataDir(), subpath);
+        if (!Directory.Exists(path))
+          Directory.CreateDirectory(path);
+        return path;
+      }
+
+      public static string GetFullPathMkParent(string path) {
+        var fullpath = Path.GetFullPath(path);
+        var dirname = Path.GetDirectoryName(fullpath);
+        if (!Directory.Exists(dirname))
+          Directory.CreateDirectory(dirname);
+        return fullpath;
+      }
+    }
+    EOF
+
+    mcs nix-support.cs -target:library -r:WFTOGame_Data/Managed/UnityEngine \
+      -out:WFTOGame_Data/Managed/NixSupport.dll
+
+    monogame-patcher replace-call \
+      -i WFTOGame_Data/Managed/Assembly-CSharp.dll \
+      'System.String UnityEngine.Application::get_dataPath()' \
+      'System.String UnityEngine.Application::get_persistentDataPath()' \
+      IniParser SaveMeta::UpdateMetaDataAndMinimap SaveMeta::GetMinimapPath \
+      SaveMeta::GetMinimapPath SaveMeta::GetMinimapAbsolutePath
+
+    monogame-patcher replace-call \
+      -i WFTOGame_Data/Managed/Assembly-CSharp.dll \
+      -a WFTOGame_Data/Managed/NixSupport.dll \
+      'System.String GameDataFolder::Get(System.String)' \
+      'System.String NixSupport::GetDataDir(System.String)' \
+      Serializer::MapSave DRMFree::FullPath DRMFree::OnEnable \
+      SaveStatAchievementLAN::FullPath SaveStatAchievementLAN::GetFolders
+
+    monogame-patcher replace-call \
+      -i WFTOGame_Data/Managed/Assembly-CSharp.dll \
+      -a WFTOGame_Data/Managed/NixSupport.dll \
+      'System.String System.IO.Path::GetFullPath(System.String)' \
+      'System.String NixSupport::GetFullPathMkParent(System.String)' \
+      SaveMeta::GetMinimapAbsolutePath
+
+    monogame-patcher fix-filestreams \
+      -i WFTOGame_Data/Managed/Assembly-CSharp-firstpass.dll \
+      UnityGTResourceHandler
+  '';
+}
diff --git a/pkgs/games/gog/warcraft2/default.nix b/pkgs/games/gog/warcraft2/default.nix
new file mode 100644
index 00000000..c576439f
--- /dev/null
+++ b/pkgs/games/gog/warcraft2/default.nix
@@ -0,0 +1,148 @@
+{ stdenv, lib, buildSandbox, writeTextFile, runCommand, fetchGog, fetchurl
+, fetchFromGitHub, winePackages, xvfb_run, ffmpeg, rename
+
+# Dependencies for the Stratagus engine
+, cmake, pkgconfig, toluapp, lua5_1, libpng, libmng, zlib, SDL, fluidsynth
+, bzip2, libmikmod, libogg, libvorbis, libtheora, libGLU_combined, sqlite
+}:
+
+let
+  timgm6mb = fetchurl {
+    name = "TimGM6mb.sf2";
+    url = "https://sourceforge.net/p/mscore/code/3412/"
+        + "tree/trunk/mscore/share/sound/TimGM6mb.sf2?format=raw";
+    sha256 = "0m68a5z43nirirq9rj2xzz6z5qpyhdwk40s83sqhr4lc09i8ndy5";
+  };
+
+  stratagus = stdenv.mkDerivation {
+    name = "stratagus-${version}";
+    version = "2.4.2git20190615";
+
+    src = fetchFromGitHub {
+      owner = "Wargus";
+      repo = "stratagus";
+      rev = "c7fc80ff7e89ab969d867121b6f679f81ea60ecb";
+      sha256 = "1n39lxd8qg03kw884llcal3h95y34lys44hib2mdb3qhd5dg9a18";
+    };
+
+    patches = [ ./xdg.patch ];
+
+    # Both check_version and detectPresence in addition to a bunch of functions
+    # in stratagus-tinyfiledialogs.h are trying to run various tools via
+    # /bin/sh, so let's NOP them out.
+    postPatch = ''
+      sed -i -e '/^int/!s/\(check_version\|detectPresence\)([^)]*)/1/g' \
+        gameheaders/stratagus-game-launcher.h
+      sed -i -e '/^\(static \+\)\?int/!s/[a-zA-Z0-9]\+Present *([^)]*)/0/g' \
+        gameheaders/stratagus-tinyfiledialogs.h
+    '';
+
+    NIX_CFLAGS_COMPILE = [ "-Wno-error=format-overflow" ];
+    cmakeFlags = [
+      "-DOpenGL_GL_PREFERENCE=GLVND" "-DENABLE_DEV=ON"
+      "-DGAMEDIR=${placeholder "out"}/bin"
+    ];
+    nativeBuildInputs = [ cmake pkgconfig toluapp ];
+    buildInputs = [
+      toluapp lua5_1 libpng libmng zlib SDL fluidsynth bzip2 libmikmod libogg
+      libvorbis libtheora libGLU_combined sqlite
+    ];
+  };
+
+  stormlib = stdenv.mkDerivation {
+    name = "stormlib-${version}";
+    version = "9.22git20190615";
+
+    src = fetchFromGitHub {
+      owner = "ladislav-zezula";
+      repo = "StormLib";
+      rev = "2f0e0e69e6b3739d7c450ac3d38816aee45ac3c2";
+      sha256 = "04f43c6bwfxiiw1kplxb3ds8g9r633y587z8ir97hrrzw5nmni3w";
+    };
+
+    nativeBuildInputs = [ cmake ];
+    buildInputs = [ zlib bzip2 ];
+  };
+
+  wargus = stdenv.mkDerivation {
+    name = "wargus-${version}";
+    version = "2.4.2git20190615";
+
+    src = fetchFromGitHub {
+      owner = "Wargus";
+      repo = "wargus";
+      rev = "932c4974dfea9805f6710f254de191e65dadb50d";
+      sha256 = "13vpfd4yx43n5sqzj79km7gcv9al3mqskkij335f0c9p28rqf47v";
+    };
+
+    # This fixes up the data path by letting it be set using an environment
+    # variable later in the wrapper and also hardcodes the MuseScore sound
+    # font.
+    #
+    # In addition, wartool.cpp contains a few lines like this:
+    #
+    #   sprintf(extract, "%s.something", extract);
+    #
+    # The problem here is that sprintf() modifies the data pointed by extract
+    # *IN PLACE*, so this essentially truncates the contents to ".something".
+    #
+    # While it may be even better to use strncat() here, the application isn't
+    # critical for security, so for the sake of laziness, let's just use
+    # strcat() instead.
+    postPatch = ''
+      sed -i -e '
+        s/^\( *# *define \+DATA_PATH\).*/\1 getenv("WARGUS_DATAPATH")/
+      ' wargus.cpp
+
+      sed -i -e 's!"[^"]*\/TimGM6mb.sf2"!"${timgm6mb}"!g' \
+        wargus.cpp scripts/stratagus.lua
+
+      sed -i -e '
+        s/sprintf( *\([^,]\+\), "%s\([^"]\+\)", *\1 *)/strcat(\1, "\2")/
+      ' wartool.cpp
+
+      # XXX: Crashes the game, see https://github.com/Wargus/wargus/issues/260
+      rm -r scripts/lists/campaigns/For\ the\ Motherland\ *
+    '';
+
+    cmakeFlags = [ "-DGAMEDIR=${placeholder "out"}/bin" ];
+    nativeBuildInputs = [ cmake pkgconfig ];
+    buildInputs = [ stratagus zlib libpng bzip2 stormlib ];
+  };
+
+  version = "2.02.4";
+
+  # XXX: Unfortunately, innoextract (even Git master) doesn't support this
+  #      archive, so let's resort to a headless wine for extraction.
+  gameData = runCommand "warcraft2-data-${version}" {
+    src = fetchGog {
+      name = "setup-warcraft-2-${version}.exe";
+      productId = 1418669891;
+      downloadName = "en1installer0";
+      sha256 = "1cf3c1ylgdrvkk7y25v47f66m6lp9m4wvl2aldpxzrrqdrlk34k3";
+    };
+    nativeBuildInputs = [ winePackages.minimal xvfb_run ffmpeg wargus rename ];
+  } ''
+    export WINEPREFIX="$PWD"
+    wine wineboot.exe
+    xvfb-run -s '-screen 0 1024x768x24' wine "$src" /sp- /lang=english /silent
+    gameDir='drive_c/GOG Games/Warcraft II BNE'
+    find "$gameDir" -mindepth 1 -depth \
+      -exec rename 's/(.*)\/([^\/]*)/$1\/\L$2/' {} \;
+    wartool -v -r "$gameDir" "$out"
+    cp -rnst "$out" ${lib.escapeShellArg wargus}/share/games/stratagus/wargus/*
+  '';
+
+in buildSandbox (writeTextFile {
+  name = "warcraft2-${version}";
+  destination = "/bin/warcraft2";
+  executable = true;
+  text = ''
+    #!${stdenv.shell}
+    export WARGUS_DATAPATH=${lib.escapeShellArg gameData}
+    exec ${lib.escapeShellArg "${wargus}/bin/wargus"} "$@"
+  '';
+}) {
+  paths.required = [ "$XDG_DATA_HOME/wc2" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" ];
+}
diff --git a/pkgs/games/gog/warcraft2/xdg.patch b/pkgs/games/gog/warcraft2/xdg.patch
new file mode 100644
index 00000000..979a86e3
--- /dev/null
+++ b/pkgs/games/gog/warcraft2/xdg.patch
@@ -0,0 +1,37 @@
+diff --git a/src/stratagus/parameters.cpp b/src/stratagus/parameters.cpp
+index a705ba21d..8585856b0 100644
+--- a/src/stratagus/parameters.cpp
++++ b/src/stratagus/parameters.cpp
+@@ -48,26 +48,13 @@ void Parameters::SetDefaultValues()
+ 
+ void Parameters::SetDefaultUserDirectory()
+ {
+-#ifdef USE_GAME_DIR
+-	userDirectory = StratagusLibPath;
+-#elif USE_WIN32
+-	userDirectory = getenv("APPDATA");
+-#else
+-	userDirectory = getenv("HOME");
+-#endif
+-
+-	if (!userDirectory.empty()) {
+-		userDirectory += "/";
++	const char *xdg_data_home = getenv("XDG_DATA_HOME");
++	if (xdg_data_home == NULL) {
++		userDirectory = getenv("HOME");
++		userDirectory += "/.local/share";
++	} else {
++		userDirectory = xdg_data_home;
+ 	}
+-
+-#ifdef USE_GAME_DIR
+-#elif USE_WIN32
+-	userDirectory += "Stratagus";
+-#elif defined(USE_MAC)
+-	userDirectory += "Library/Stratagus";
+-#else
+-	userDirectory += ".stratagus";
+-#endif
+ }
+ 
+ static std::string GetLocalPlayerNameFromEnv()
diff --git a/pkgs/games/gog/wizard-of-legend.nix b/pkgs/games/gog/wizard-of-legend.nix
new file mode 100644
index 00000000..ab754f13
--- /dev/null
+++ b/pkgs/games/gog/wizard-of-legend.nix
@@ -0,0 +1,14 @@
+{ buildUnity, fetchGog }:
+
+buildUnity {
+  name = "wizard-of-legend";
+  fullName = "WizardOfLegend";
+  saveDir = "Contingent99/Wizard of Legend";
+  version = "1.033b";
+
+  src = fetchGog {
+    productId = 2061814323;
+    downloadName = "en3installer0";
+    sha256 = "192fhway7ij5f4fh0vb1204f3yg3fxz08fvqlg03gskjs9krcbcz";
+  };
+}
diff --git a/pkgs/games/gog/xeen.nix b/pkgs/games/gog/xeen.nix
new file mode 100644
index 00000000..e5e65fc8
--- /dev/null
+++ b/pkgs/games/gog/xeen.nix
@@ -0,0 +1,119 @@
+{ lib, stdenv, buildSandbox, fetchGog, gogUnpackHook, bchunk, p7zip
+, scummvm, fetchFromGitHub
+, runCommand, xvfb_run
+
+, showItemCosts ? true
+, durableArmor ? true
+}:
+
+let
+  version = "2.1.0.43";
+
+  gameData = stdenv.mkDerivation {
+    name = "world-of-xeen-gamedata-${version}";
+    inherit version;
+
+    src = fetchGog {
+      name = "setup_mm45_${version}.exe";
+      productId = 1207661233;
+      downloadName = "en1installer1";
+      sha256 = "0jv9k5rcapqlk61pawa5l4m34iwllx8j6cfz69gl092h04fvfqki";
+    };
+
+    nativeBuildInputs = [ gogUnpackHook ];
+    innoExtractOnly = [ "app/game1.gog" "app/music" ];
+
+    patchPhase = ''
+      cat > game1.inst <<EOF
+      FILE "game1.gog" BINARY
+      TRACK 01 MODE1/2352
+        INDEX 01 00:00:00
+      EOF
+    '';
+
+    buildPhase = ''
+      ${bchunk}/bin/bchunk game1.gog game1.inst game_cd
+      ${p7zip}/bin/7z x game_cd01.iso
+    '';
+
+    installPhase = ''
+      for i in [Gg][Aa][Mm][Ee]/*.[Cc][Cc]; do
+        filename="$(basename "$i")"
+        install -vD -m 0644 "$i" "$out/''${filename,,}"
+      done
+
+      for i in music/*.ogg; do
+        filename="$(basename "$i")"
+        install -vD -m 0644 "$i" "$out/''${filename/xeen/track}"
+      done
+    '';
+
+    doInstallCheck = true;
+
+    installCheckPhase = ''
+      ccFileNo="$(ls -1 "$out/"*.cc | wc -l)"
+      if [ "$ccFileNo" -ne 3 ]; then
+        echo "Expected three .cc files, but got $ccFileNo." >&2
+        ls -l "$out"
+        exit 1
+      fi
+      trackFileNo="$(ls -1 "$out/"track[0-9][0-9].ogg | wc -l)"
+      if [ "$trackFileNo" -ne 59 ]; then
+        echo "Expected 59 track[0-9][0-9].ogg files, but got $trackFileNo." >&2
+        ls -l "$out"
+        exit 1
+      fi
+    '';
+  };
+
+  latestScummVM = scummvm.overrideAttrs (drv: {
+    src = fetchFromGitHub {
+      owner = "scummvm";
+      repo = "scummvm";
+      rev = "ca8b79fa751d1f8eac1e468936cbf1f5d7656674";
+      sha256 = "0aa12n3mci7zw2mhh23721ixx0b8zh5463a529s2rkf9wjq751f0";
+    };
+
+    configureFlags = (drv.configureFlags or []) ++ [
+      "--disable-all-engines" "--enable-engine=xeen"
+    ];
+
+    # Current Git version has an --enable-static option so the stdenv setup
+    # thinks that there is --disable-static as well, which is not.
+    dontDisableStatic = true;
+  });
+
+  injectOption = c: o: lib.optionalString c "-e '/^\\[worldof/a ${o}=true'";
+
+  scummVmConfig = runCommand "scummvm-xeen.ini" {
+    nativeBuildInputs = [ xvfb_run latestScummVM ];
+    inherit gameData;
+  } ''
+    xvfb-run scummvm -p "$gameData" -a
+    sed -e '/^\[scummvm\]/a enable_unsupported_game_warning=false' \
+      ${injectOption showItemCosts "ShowItemCosts"} \
+      ${injectOption durableArmor "DurableArmor"} \
+      scummvm.ini > "$out"
+  '';
+
+  unsandboxed = runCommand "word-of-xeen-${version}" {
+    scummVmCmd = "${latestScummVM}/bin/scummvm";
+    dataHome = "\"\${XDG_DATA_HOME:-$HOME/.local/share}/xeen\"";
+    inherit (stdenv) shell;
+    scummVmArgs = lib.concatMapStringsSep " " lib.escapeShellArg [
+      "-c" scummVmConfig
+    ];
+  } ''
+    mkdir -p "$out/bin"
+    cat > "$out/bin/xeen" <<EOF
+    #!$shell
+    exec $scummVmCmd $scummVmArgs --savepath=$dataHome "\$@" worldofxeen-cd
+    EOF
+    chmod +x "$out/bin/xeen"
+    cat "$out/bin/xeen"
+  '';
+
+in buildSandbox unsandboxed {
+  paths.required = [ "$XDG_DATA_HOME/xeen" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" ];
+}
diff --git a/pkgs/games/humblebundle/antichamber.nix b/pkgs/games/humblebundle/antichamber.nix
new file mode 100644
index 00000000..32e6ad2d
--- /dev/null
+++ b/pkgs/games/humblebundle/antichamber.nix
@@ -0,0 +1,56 @@
+{ stdenv, lib, fetchHumbleBundle, unzip
+, xorg, libpulseaudio, libvorbis, libogg, libGL }:
+
+stdenv.mkDerivation rec {
+  name = "antichamber-1.1";
+
+  src = fetchHumbleBundle {
+    name = "antichamber_1.01_linux_1392664980.sh";
+    machineName = "antichamber_linux";
+    md5 = "37bca01c411d813c8729259b7db2dba0";
+  };
+
+  dontStrip = true;
+  phases = ["installPhase"];
+
+  installPhase = let
+    rpath = lib.makeLibraryPath [
+      "$dest/Binaries/Linux"
+      xorg.libX11
+      xorg.libXi
+      stdenv.cc.cc.lib
+      libpulseaudio
+      libvorbis
+      libogg
+      libGL
+    ];
+   in ''
+    dest="$out/share/antichamber"
+
+    # Unpack binaries and data into $dest
+    mkdir -p "$dest"
+    ${unzip}/bin/unzip $src "data/*" -d $dest && [ $? -le 2 ]
+    mv $dest/data/*/* $dest
+    rm -r $dest/data
+
+    # Patch heavily :-)
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}" "$dest/Binaries/Linux/UDKGame-Linux"
+    for exe in $dest/Binaries/Linux/lib/*.so{,.*} ; do
+      patchelf --set-rpath "${rpath}" "$exe"
+    done
+
+    # Fixup permissions, just to be sure.
+    find "$dest" -type f -exec chmod 644 "{}" +
+    find "$dest/Binaries" -type f -exec chmod 755 "{}" +
+
+    mkdir -p "$out/bin"
+    cat > $out/bin/antichamber <<EOF
+    #!${stdenv.shell}
+    cd $dest/Binaries/Linux/
+    exec ./UDKGame-Linux "$@"
+    EOF
+    chmod 755 $out/bin/antichamber
+  '';
+}
diff --git a/pkgs/games/humblebundle/baba-is-you.nix b/pkgs/games/humblebundle/baba-is-you.nix
new file mode 100644
index 00000000..af8da736
--- /dev/null
+++ b/pkgs/games/humblebundle/baba-is-you.nix
@@ -0,0 +1,27 @@
+{ stdenv, buildGame, fetchHumbleBundle, libGL, makeWrapper }:
+
+buildGame rec {
+  name = "baba-is-you-unstable-${version}";
+  version = "2019-08-07"; # guessed from aug07 in file name.
+
+  src = fetchHumbleBundle {
+    name = "BIY_linux_aug07.tar.gz";
+    machineName = "babaisyou_odtbg_linux_oR9qb";
+    downloadName = "Download";
+    suffix = "tar.gz";
+    md5 = "3694afc5579cdaad7448c9744aa8d063";
+  };
+
+  buildInputs = [ makeWrapper libGL ];
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/Baba_Is_You" ];
+
+  installPhase = ''
+    mkdir -p "$out/bin" "$out/share/baba-is-you"
+    rm -r bin32
+
+    cp -vrt "$out/share/baba-is-you" .
+    makeWrapper "$out/share/baba-is-you/bin64/Chowdren" "$out/bin/baba-is-you" \
+      --run "cd '$out/share/baba-is-you'"
+  '';
+}
diff --git a/pkgs/games/humblebundle/bastion.nix b/pkgs/games/humblebundle/bastion.nix
new file mode 100644
index 00000000..207dd105
--- /dev/null
+++ b/pkgs/games/humblebundle/bastion.nix
@@ -0,0 +1,98 @@
+{ lib, stdenv, buildGame, fixFmodHook, fetchHumbleBundle, makeWrapper
+, unzip, imagemagick, mono, SDL2, SDL2_image, openal, libvorbis, libGL
+}:
+
+buildGame rec {
+  name = "bastion-${version}";
+  version = "20161016";
+
+  src = fetchHumbleBundle {
+    name = "bastion-20161016.bin";
+    machineName = "bastion_linux";
+    downloadName = "Download";
+    md5 = "19fea173ff2da0f990f60bd5e7c3b237";
+  };
+
+  unpackCmd = "${unzip}/bin/unzip -qq \"$curSrc\" 'data/*' || :";
+
+  nativeBuildInputs = [ makeWrapper imagemagick fixFmodHook ];
+
+  libDir =
+    if stdenv.system == "i686-linux" then "lib"
+    else if stdenv.system == "x86_64-linux" then "lib64"
+    else throw "Unsupported architecture ${stdenv.system}.";
+
+  buildPhase = let
+    dllmap = {
+      SDL2 = "${SDL2}/lib/libSDL2.so";
+      SDL2_image = "${SDL2_image}/lib/libSDL2_image.so";
+      soft_oal = "${openal}/lib/libopenal.so";
+      libvorbisfile = "${libvorbis}/lib/libvorbisfile.so";
+      MojoShader = "$out/libexec/bastion/libmojoshader.so";
+    };
+  in lib.concatStrings (lib.mapAttrsToList (dll: target: ''
+    sed -i -e '/<dllmap.*dll="${dll}\.dll".*os="linux"/ {
+      s!target="[^"]*"!target="'"${target}"'"!
+    }' FNA.dll.config
+  '') dllmap) + ''
+    # This is not needed, so let's remove it.
+    sed -i -e '/libtheoraplay/d' FNA.dll.config
+
+    # We don't want any Steam libraries, that's why we bought the non-DRM
+    # version, right?
+    cc -Wall -std=c11 -shared -xc - -o "$libDir/libSteamStub.so" -fPIC <<EOF
+    #include <stddef.h>
+    int SteamAPI_Init(void) { return 0; }
+    void SteamAPI_Shutdown(void) {}
+    void SteamAPI_RunCallbacks() {}
+    /* All the symbols for the global accessors for Steamworks C++ APIs */
+    ${lib.concatMapStrings (sym: ''
+      void *Steam${sym}(void) { return NULL; }
+    '') [
+      "Client" "User" "Friends" "Utils" "Matchmaking" "UserStats" "Apps"
+      "Networking" "MatchmakingServers" "RemoteStorage" "Screenshots" "HTTP"
+      "UnifiedMessages" "Controller" "UGC" "AppList" "Music" "MusicRemote"
+      "HTMLSurface" "Inventory" "Video"
+    ]}
+    EOF
+    patchelf --remove-needed libsteam_api.so "$libDir/libSteamWrapper.so"
+    patchelf --add-needed libSteamStub.so "$libDir/libSteamWrapper.so"
+
+    # For the Desktop icon
+    convert Bastion.bmp Bastion.png
+  '';
+
+  installPhase = ''
+    for lib in fmodex mojoshader SteamWrapper SteamStub; do
+      install -vD "$libDir/lib$lib.so" "$out/libexec/bastion/lib$lib.so"
+    done
+
+    cp -rvt "$out/libexec/bastion" Bastion.exe FNA.dll* \
+      FMOD.dll Lidgren.Network.dll MonoGame.Framework.Net.dll
+
+    mkdir -p "$out/share"
+    cp -rv Content "$out/share/bastion"
+    cp -vt "$out/libexec/bastion" Bastion.bmp
+    ln -s "$out/share/bastion" "$out/libexec/bastion/Content"
+
+    makeWrapper ${lib.escapeShellArg mono}/bin/mono "$out/bin/bastion" \
+      --add-flags "$out/libexec/bastion/Bastion.exe" \
+      --set SDL_OPENGL_LIBRARY ${lib.escapeShellArg "${libGL}/lib/libGL.so"} \
+      --run "cd '$out/libexec/bastion'"
+
+    install -vD -m 0644 Bastion.png "$out/share/icons/bastion.png"
+
+    mkdir -p "$out/share/applications"
+    cat > "$out/share/applications/bastion.desktop" <<EOF
+    [Desktop Entry]
+    Name=Bastion
+    Type=Application
+    Version=1.1
+    Exec=$out/bin/bastion
+    Icon=$out/share/icons/bastion.png
+    Categories=Game
+    EOF
+  '';
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/Bastion" ];
+}
diff --git a/pkgs/games/humblebundle/brigador.nix b/pkgs/games/humblebundle/brigador.nix
new file mode 100644
index 00000000..71fbea28
--- /dev/null
+++ b/pkgs/games/humblebundle/brigador.nix
@@ -0,0 +1,103 @@
+{ stdenv, fetchurl, makeWrapper, fetchHumbleBundle, writeText
+, SDL2, libGL, glew, freeimage
+}:
+
+let
+  oldGLEW = glew.overrideDerivation (stdenv.lib.const rec {
+    name = "glew-1.12.0";
+    src = fetchurl {
+      url = "mirror://sourceforge/glew/${name}.tgz";
+      sha256 = "1gz4917k9iyv3s8k0fxylzrwdnlf7dcszlsfzbkl7d1490zi0n5g";
+    };
+  });
+
+in stdenv.mkDerivation {
+  name = "brigador-1.0";
+
+  src = fetchHumbleBundle {
+    machineName = "brigador_linux";
+    suffix = "tar";
+    md5 = "61af4a5f037b85bf6acc5ca76d295d09";
+  };
+
+  preloader = writeText "brigador-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+    #include <fcntl.h>
+    #include <stdarg.h>
+    #include <stdio.h>
+    #include <string.h>
+    #include <sys/stat.h>
+
+    #define MANGLE_PATH(call, ...) \
+      if (strncmp(path, "assets.pack", 12) == 0 || \
+          strncmp(path, "assets/",     7)  == 0 || \
+          strncmp(path, "fonts/",      6)  == 0 || \
+          strncmp(path, "shaders/",    8)  == 0 || \
+          strncmp(path, "sounds/",     7)  == 0) { \
+        char buf[1024]; \
+        snprintf(buf, sizeof(buf), "%s/%s", LIBEXEC_PATH, path); \
+        return call(buf, __VA_ARGS__); \
+      }
+
+    ${stdenv.lib.concatMapStrings (fun: ''
+      FILE *${fun}(const char *path, const char *mode) {
+        static FILE *(*_${fun}) (const char *, const char *) = NULL;
+        if (_${fun} == NULL) _${fun} = dlsym(RTLD_NEXT, "${fun}");
+        MANGLE_PATH(_${fun}, mode);
+        return _${fun}(path, mode);
+      }
+    '') [ "fopen" "fopen64" ]}
+
+    int open(const char *path, int flags, ...) {
+      va_list ap;
+      mode_t mode;
+      static int (*_open) (const char *, int, mode_t) = NULL;
+      if (_open == NULL) _open = dlsym(RTLD_NEXT, "open");
+      va_start(ap, flags);
+      mode = va_arg(ap, mode_t);
+      va_end(ap);
+      MANGLE_PATH(_open, (flags & ~O_RDWR) | O_RDONLY, mode);
+      return _open(path, flags, mode);
+    }
+  '';
+
+  patchPhase = let
+    fmodRpath = stdenv.lib.makeLibraryPath [ "$out" stdenv.cc.cc ];
+    rpath = stdenv.lib.makeLibraryPath [ "$out" SDL2 libGL oldGLEW freeimage ];
+  in ''
+    for fmod in lib/libfmod*.so*; do
+      patchelf --set-rpath "${fmodRpath}" "$fmod"
+    done
+
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}" brigador
+  '';
+
+  buildPhase = ''
+    cc -Werror -shared "$preloader" -o preloader.so -ldl -fPIC \
+      -DLIBEXEC_PATH=\"$out/libexec/brigador\"
+  '';
+
+  buildInputs = [ makeWrapper ];
+
+  installPhase = ''
+    for fmod in lib/libfmod*.so*; do
+      install -vD "$fmod" "$out/lib/$(basename "$fmod")"
+    done
+
+    install -vD brigador "$out/libexec/brigador/brigador"
+    install -vD preloader.so "$out/libexec/brigador/preloader.so"
+    install -vD -m 0644 assets.pack "$out/libexec/brigador/assets.pack"
+    cp -rt "$out/libexec/brigador" assets fonts shaders sounds
+
+    makeWrapper "$out/libexec/brigador/brigador" "$out/bin/brigador" \
+      --set LD_PRELOAD "$out/libexec/brigador/preloader.so" \
+      --run 'XDG_DATA_HOME="''${XDG_DATA_HOME:-$HOME/.local/share}"' \
+      --run 'mkdir -p "$XDG_DATA_HOME/brigador"' \
+      --run 'cd "$XDG_DATA_HOME/brigador"'
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/cavestoryplus.nix b/pkgs/games/humblebundle/cavestoryplus.nix
new file mode 100644
index 00000000..a549ebfb
--- /dev/null
+++ b/pkgs/games/humblebundle/cavestoryplus.nix
@@ -0,0 +1,39 @@
+{ stdenv, fetchHumbleBundle, makeWrapper, SDL, libGL }:
+
+stdenv.mkDerivation rec {
+  name = "cave-story-plus-${version}";
+  version = "r100";
+
+  src = fetchHumbleBundle {
+    machineName = "cavestoryplus_linux";
+    downloadName = ".tar.bz2";
+    suffix = "tar.bz2";
+    md5 = "b7ecd65644b8607bc177d7ce670f2185";
+  };
+
+  buildInputs = [ makeWrapper ];
+
+  patchPhase = let
+    rpath = stdenv.lib.makeLibraryPath [
+      SDL "$out" stdenv.cc.cc libGL
+    ];
+  in ''
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}" CaveStory+_64
+  '';
+
+  installPhase = ''
+    install -vD CaveStory+_64 "$out/libexec/cave-story-plus/cave-story-plus"
+    mkdir -p "$out/bin"
+    makeWrapper \
+      "$out/libexec/cave-story-plus/cave-story-plus" \
+      "$out/bin/cave-story-plus" \
+      --run "cd '$out/share/cave-story-plus'"
+
+    mkdir -p "$out/share/cave-story-plus"
+    cp -vrt "$out/share/cave-story-plus" data
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/default.nix b/pkgs/games/humblebundle/default.nix
new file mode 100644
index 00000000..5de3f40e
--- /dev/null
+++ b/pkgs/games/humblebundle/default.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.humblebundle;
+
+  self = rec {
+    callPackage = pkgs.lib.callPackageWith (pkgs // self);
+    callPackage_i686 = pkgs.lib.callPackageWith (pkgs.pkgsi686Linux // self);
+
+    fetchHumbleBundle = callPackage ./fetch-humble-bundle {
+      inherit (config.humblebundle) email password;
+    };
+
+    antichamber = callPackage_i686 ./antichamber.nix { };
+    baba-is-you = callPackage ./baba-is-you.nix { };
+    bastion = callPackage ./bastion.nix {};
+    brigador = callPackage ./brigador.nix {};
+    cavestoryplus = callPackage ./cavestoryplus.nix {};
+    dott = callPackage_i686 ./dott.nix {};
+    fez = callPackage ./fez.nix {};
+    ftl = callPackage ./ftl.nix {};
+    guacamelee = callPackage_i686 ./guacamelee.nix {};
+    grim-fandango = callPackage_i686 ./grim-fandango.nix {};
+    hammerwatch = callPackage ./hammerwatch.nix {};
+    jamestown = callPackage ./jamestown.nix {};
+    liads = callPackage ./liads.nix {};
+    megabytepunch = callPackage_i686 ./megabytepunch.nix {};
+    minimetro = callPackage ./minimetro.nix {};
+    opus-magnum = callPackage ./opus-magnum.nix {};
+    owlboy = callPackage ./owlboy.nix {};
+    pico-8 = callPackage ./pico-8.nix {};
+    rocketbirds = callPackage ./rocketbirds.nix {};
+    spaz = callPackage ./spaz.nix {};
+    starbound = callPackage ./starbound.nix {};
+    swordsandsoldiers = callPackage ./swordsandsoldiers.nix {};
+    the-bridge = callPackage_i686 ./the-bridge.nix {};
+    trine2 = callPackage_i686 ./trine2.nix {};
+    unepic = callPackage ./unepic.nix {};
+  };
+in with lib; {
+  options.humblebundle = {
+    email = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Email address for your HumbleBundle account.
+      '';
+    };
+
+    password = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Password for your HumbleBundle account.
+      '';
+    };
+  };
+
+  config.packages = {
+    humblebundle = mkIf (cfg.email != null && cfg.password != null) self;
+  };
+}
diff --git a/pkgs/games/humblebundle/dott.nix b/pkgs/games/humblebundle/dott.nix
new file mode 100644
index 00000000..eef7fb87
--- /dev/null
+++ b/pkgs/games/humblebundle/dott.nix
@@ -0,0 +1,70 @@
+{ stdenv, fetchHumbleBundle, libGL, libpulseaudio, alsaLib, libudev
+, writeText
+}:
+
+stdenv.mkDerivation rec {
+  name = "dott-remastered-${version}";
+  version = "1.4.1";
+
+  src = fetchHumbleBundle {
+    machineName = "dayofthetentacle_linux_beYLi";
+    suffix = "tar.gz";
+    md5 = "667b2a8a082702832242321515e55e70";
+  };
+
+  unpackCmd = "mkdir \"$name\" && tar xf \"$curSrc\" -C \"$name\"";
+
+  preloader = writeText "dott-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+    #include <fcntl.h>
+    #include <string.h>
+    #include <unistd.h>
+
+    static int datadir_size = sizeof(DOTT_DATADIR) - 1;
+
+    ssize_t readlink(const char *path, char *buf, size_t bufsize) {
+      static ssize_t (*_readlink) (const char *, char *, size_t) = NULL;
+      if (_readlink == NULL) _readlink = dlsym(RTLD_NEXT, "readlink");
+
+      if (strncmp(path, "/proc/self/exe", 15) == 0) {
+        ssize_t copylen = datadir_size > bufsize ? bufsize : datadir_size;
+        memcpy(buf, DOTT_DATADIR, copylen);
+        return copylen;
+      } else {
+        return _readlink(path, buf, bufsize);
+      }
+    }
+
+    int SteamAPI_Init(void) {
+      return 0;
+    }
+  '';
+
+  rpath = stdenv.lib.makeLibraryPath [
+    libGL stdenv.cc.cc libpulseaudio alsaLib.out libudev
+  ];
+
+  buildPhase = ''
+    cc -Werror -Wall -std=gnu11 -shared "$preloader" -o preload.so -fPIC \
+      -DDOTT_DATADIR="\"$out/share/dott/DUMMY\""
+    patchelf --set-rpath "$rpath" lib/libfmod.so.8
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --remove-needed libsteam_api.so \
+      --add-needed "$out/libexec/dott/libdott-preload.so" \
+      Dott
+    patchelf --set-rpath "$out/libexec/dott:$rpath" Dott
+  '';
+
+  installPhase = ''
+    install -vsD preload.so "$out/libexec/dott/libdott-preload.so"
+    install -vD lib/libfmod.so.8 "$out/libexec/dott/libfmod.so.8"
+    install -vD Dott "$out/bin/dott"
+    if ldd "$out/bin/dott" | grep -F 'not found'; then exit 1; fi
+    install -vD -m 0644 tenta.cle "$out/share/dott/tenta.cle"
+  '';
+
+  dontStrip = true;
+  dontPatchELF = true;
+}
diff --git a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
new file mode 100644
index 00000000..1340bce8
--- /dev/null
+++ b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
@@ -0,0 +1,256 @@
+{ stdenv, curl, cacert, writeText, writeScript, fetchFromGitHub, fetchpatch
+, python, python3, pythonPackages
+
+# Dependencies for the captcha solver
+, pkgconfig, qt5
+
+, email, password
+}:
+
+{ name ? null
+, machineName
+, downloadName ? "Download"
+, suffix ? "humblebundle"
+, md5
+}:
+
+let
+  cafile = "${cacert}/etc/ssl/certs/ca-bundle.crt";
+
+  getCaptcha = let
+    injectedJS = ''
+      function waitForResponse() {
+        try {
+          var response = grecaptcha.getResponse();
+        } catch(_) {
+          return setTimeout(waitForResponse, 50);
+        }
+        if (response != "")
+          document.title = response;
+        else
+          setTimeout(waitForResponse, 50);
+      }
+
+      waitForResponse();
+    '';
+
+    escapeCString = stdenv.lib.replaceStrings ["\"" "\n"] ["\\\"" "\\n"];
+
+    application = writeText "captcha.cc" ''
+      #include <QApplication>
+      #include <QWebEngineView>
+      #include <QTcpServer>
+      #include <QQuickWebEngineProfile>
+
+      int main(int argc, char **argv) {
+        QApplication *app = new QApplication(argc, argv);
+        QTcpServer *server = new QTcpServer();
+        QWebEngineView *browser = new QWebEngineView();
+
+        QQuickWebEngineProfile::defaultProfile()->setOffTheRecord(true);
+
+        if (!server->listen(QHostAddress::LocalHost, 18123)) {
+          qCritical() << "Unable to listen on port 18123!";
+          return 1;
+        }
+
+        qInfo() << "Waiting for connection from the HB downloader...";
+        if (!server->waitForNewConnection(-1)) {
+          qCritical() << "Unable to accept the connection!";
+          return 1;
+        }
+        qInfo() << "Connection established, spawning window to solve captcha.";
+
+        QTcpSocket *sock = server->nextPendingConnection();
+
+        browser->load(QUrl("https://www.humblebundle.com/user/captcha"));
+        browser->show();
+
+        browser->connect(browser, &QWebEngineView::loadFinished, [=]() {
+          browser->page()->runJavaScript("${escapeCString injectedJS}");
+          browser->connect(
+            browser, &QWebEngineView::titleChanged, [=](const QString &title) {
+              sock->write(title.toUtf8());
+              sock->flush();
+              sock->waitForBytesWritten();
+              sock->close();
+              server->close();
+              app->quit();
+            }
+          );
+        });
+
+        return app->exec();
+      }
+    '';
+
+  in stdenv.mkDerivation {
+    name = "get-captcha";
+
+    dontUnpack = true;
+
+    nativeBuildInputs = [ pkgconfig (qt5.wrapQtAppsHook or null) ];
+    buildInputs = [ qt5.qtbase qt5.qtwebengine ];
+    preferLocalBuild = true;
+
+    buildPhase = ''
+      g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
+        -Wall -std=c++11 -o get-captcha ${application}
+    '';
+
+    installPhase = ''
+      install -vD get-captcha "$out/bin/get-captcha"
+    '';
+  };
+
+  getGuard = writeScript "get-guard" ''
+    #!${python3.interpreter}
+    import socket
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+      sock.bind(('localhost', 18129))
+      sock.listen(1)
+      with sock.accept()[0] as conn:
+        guard = input("Guard code: ")
+        conn.sendall(guard.encode())
+  '';
+
+  humbleAPI = pythonPackages.buildPythonPackage rec {
+    name = "humblebundle-${version}";
+    version = "0.1.1";
+
+    src = fetchFromGitHub {
+      owner = "saik0";
+      repo = "humblebundle-python";
+      rev = version;
+      sha256 = "1kcg42nh7sbjabim1pbqx14468pypznjy7fx2bv7dicy0sqd9b8j";
+    };
+
+    patches = [ ./guard-code.patch ];
+
+    postPatch = ''
+      sed -i -e '/^LOGIN_URL *=/s,/login,/processlogin,' humblebundle/client.py
+      sed -i -e '/self\.supports_canonical.*data.*supports_canonical/d' \
+        humblebundle/models.py
+    '';
+
+    propagatedBuildInputs = [ pythonPackages.requests ];
+  };
+
+  pyStr = str: "'${stdenv.lib.escape ["'" "\\"] str}'";
+
+  getDownloadURL = writeText "gethburl.py" ''
+    import socket, sys, time, humblebundle
+
+    def get_products(client):
+      gamekeys = client.get_gamekeys()
+      for gamekey in gamekeys:
+        order = hb.get_order(gamekey)
+        if order.subproducts is None:
+          continue
+        for subproduct in order.subproducts:
+          prodname = subproduct.human_name.encode('ascii', 'replace')
+          downloads = [(download.machine_name, download.download_struct)
+                       for download in subproduct.downloads]
+          yield ((subproduct.machine_name, prodname), downloads)
+
+    def find_download(downloads):
+      for machine_name, dstruct in sum(downloads.values(), []):
+        if machine_name == ${pyStr machineName}:
+          for ds in dstruct:
+            if ds.name == ${pyStr downloadName}:
+              return ds
+          print >>sys.stderr, \
+            ${pyStr "Unable to find ${downloadName} for ${machineName}!"}
+          print >>sys.stderr, 'Available download types:'
+          for ds in dstruct:
+            print >>sys.stderr, "  " + ds.name
+          raise SystemExit(1)
+
+    def login_with_captcha(hb):
+      print >>sys.stderr, "Solving a captcha is required to log in."
+      print >>sys.stderr, "Please run " ${pyStr (toString getCaptcha)} \
+                          "/bin/get-captcha now."
+      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      print >>sys.stderr, "Waiting for connection",
+      i = 0
+      while sock.connect_ex(("127.0.0.1", 18123)) != 0:
+        time.sleep(0.1)
+        if i % 10 == 0:
+          sys.stderr.write('.')
+          sys.stderr.flush()
+        i += 1
+      print >>sys.stderr, " connected."
+      print >>sys.stderr, "Waiting for captcha to be solved..."
+      response = sock.recv(4096)
+      sock.close()
+      print >>sys.stderr, "Captcha solved correctly, logging in."
+      api_login(hb, recaptcha_response=response)
+
+    def login_with_guard(hb, skip_code):
+      print >>sys.stderr, "A guard code has been sent to your email address."
+      print >>sys.stderr, "Please run " ${pyStr (toString getGuard)} " now."
+      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      print >>sys.stderr, "Waiting for connection",
+      # XXX: DRY!
+      i = 0
+      while sock.connect_ex(("127.0.0.1", 18129)) != 0:
+        time.sleep(0.1)
+        if i % 10 == 0:
+          sys.stderr.write('.')
+          sys.stderr.flush()
+        i += 1
+      print >>sys.stderr, " connected."
+      print >>sys.stderr, "Waiting for guard code..."
+      response = sock.recv(4096)
+      sock.close()
+      print >>sys.stderr, "Guard code supplied, logging in."
+      api_login(hb, captcha_skip_code=skip_code, guard_code=response)
+
+    def api_login(hb, recaptcha_response=None,
+                  captcha_skip_code=None, guard_code=None):
+      try:
+        hb.login(${pyStr email}, ${pyStr password},
+                 recaptcha_response=recaptcha_response,
+                 captcha_skip_code=captcha_skip_code, guard_code=guard_code)
+      except humblebundle.exceptions.HumbleCaptchaException:
+        login_with_captcha(hb)
+      except humblebundle.exceptions.HumbleGuardRequiredException as e:
+        login_with_guard(hb, e.captcha_skip_code)
+
+    hb = humblebundle.HumbleApi()
+    api_login(hb)
+
+    products = dict(get_products(hb))
+    dstruct = find_download(products)
+
+    if dstruct is None:
+      print >>sys.stderr, ${pyStr "Cannot find download for ${machineName}!"}
+      print >>sys.stderr, 'Available machine names:'
+      for name, dstructs in sorted(products.items(), key=lambda x: x[0]):
+        print >>sys.stderr, "  * " + name[1]
+        print >>sys.stderr, "    " + ', '.join(map(lambda x: x[0], dstructs))
+      raise SystemExit(1)
+    elif dstruct.md5 != ${pyStr md5}:
+      print >>sys.stderr, \
+        ${pyStr "MD5 for ${machineName} is not ${md5} but "} \
+        + dstruct.md5 + '.'
+      raise SystemExit(1)
+    else:
+      print dstruct.url.web
+  '';
+in stdenv.mkDerivation {
+  name = if name != null then name else "${machineName}.${suffix}";
+  outputHashAlgo = "md5";
+  outputHash = md5;
+
+  preferLocalBuild = true;
+  buildInputs = [ python humbleAPI ];
+
+  buildCommand = ''
+    url="$(python "${getDownloadURL}")"
+    header "downloading $name from $url"
+    "${curl.bin or curl}/bin/curl" --cacert "${cafile}" --fail \
+      --output "$out" "$url"
+    stopNest
+  '';
+}
diff --git a/pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch b/pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch
new file mode 100644
index 00000000..f17928ae
--- /dev/null
+++ b/pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch
@@ -0,0 +1,121 @@
+diff --git a/humblebundle/client.py b/humblebundle/client.py
+index fbc31c9..44184a1 100644
+--- a/humblebundle/client.py
++++ b/humblebundle/client.py
+@@ -75,7 +75,9 @@ class HumbleApi(object):
+     """
+ 
+     @callback
+-    def login(self, username, password, authy_token=None, recaptcha_challenge=None, recaptcha_response=None,
++    def login(self, username, password, authy_token=None,
++              recaptcha_challenge=None, recaptcha_response=None,
++              guard_code=None, captcha_skip_code=None,
+               *args, **kwargs):
+         """
+         Login to the Humble Bundle API. The response sets the _simpleauth_sess cookie which is stored in the session
+@@ -87,6 +89,8 @@ class HumbleApi(object):
+         :type authy_token: integer or str
+         :param str recaptcha_challenge: (optional) The challenge signed by Humble Bundle's public key from reCAPTCHA
+         :param str recaptcha_response: (optional) The plaintext solved CAPTCHA
++        :param str guard_code: (optional) The guard code sent via email
++        :param str captcha_skip_code: (optional) A token to skip the CAPTCHA
+         :param list args: (optional) Extra positional args to pass to the request
+         :param dict kwargs: (optional) Extra keyword args to pass to the request. If a data dict is supplied a key
+                             collision with any of the above params will resolved in favor of the supplied param
+@@ -108,7 +112,9 @@ class HumbleApi(object):
+             'password': password,
+             'authy-token': authy_token,
+             'recaptcha_challenge_field': recaptcha_challenge,
+-            'recaptcha_response_field': recaptcha_response}
++            'recaptcha_response_field': recaptcha_response,
++            'guard': guard_code,
++            'captcha-skip-code': captcha_skip_code}
+         kwargs.setdefault('data', {}).update({k: v for k, v in default_data.items() if v is not None})
+ 
+         response = self._request('POST', LOGIN_URL, *args, **kwargs)
+diff --git a/humblebundle/exceptions.py b/humblebundle/exceptions.py
+index 9041219..fe4eeaf 100644
+--- a/humblebundle/exceptions.py
++++ b/humblebundle/exceptions.py
+@@ -9,7 +9,7 @@ __copyright__ = "Copyright 2014, Joel Pedraza"
+ __license__ = "MIT"
+ 
+ __all__ = ['HumbleException', 'HumbleResponseException', 'HumbleAuthenticationException', 'HumbleCredentialException',
+-           'HumbleCaptchaException', 'HumbleTwoFactorException', 'HumbleParseException']
++           'HumbleCaptchaException', 'HumbleTwoFactorException', 'HumbleGuardRequiredException', 'HumbleParseException']
+ 
+ from requests import RequestException
+ 
+@@ -38,6 +38,7 @@ class HumbleAuthenticationException(HumbleResponseException):
+     def __init__(self, *args, **kwargs):
+         self.captcha_required = kwargs.pop('captcha_required', None)
+         self.authy_required = kwargs.pop('authy_required', None)
++        self.captcha_skip_code = kwargs.pop('captcha_skip_code', None)
+         super(HumbleAuthenticationException, self).__init__(*args, **kwargs)
+ 
+ 
+@@ -62,6 +63,13 @@ class HumbleTwoFactorException(HumbleAuthenticationException):
+     pass
+ 
+ 
++class HumbleGuardRequiredException(HumbleAuthenticationException):
++    """
++    A guard code is required
++    """
++    pass
++
++
+ class HumbleParseException(HumbleResponseException):
+     """
+     An error occurred while parsing
+diff --git a/humblebundle/handlers.py b/humblebundle/handlers.py
+index 36fc6e1..a8acebf 100644
+--- a/humblebundle/handlers.py
++++ b/humblebundle/handlers.py
+@@ -64,29 +64,42 @@ def login_handler(client, response):
+     success = data.get('success', None)
+     if success is True:
+         return True
++    if data.get('goto', None) is not None:
++        return True
+ 
+     captcha_required = data.get('captcha_required')
+     authy_required = data.get('authy_required')
++    captcha_skip_code = data.get('skip_code', [None])[0]
++
++    guard = data.get('humble_guard_required', False)
++    if guard:
++        raise HumbleGuardRequiredException('Guard code required', request=response.request, response=response,
++                                           captcha_required=captcha_required, authy_required=authy_required,
++                                           captcha_skip_code=captcha_skip_code)
+ 
+     errors, error_msg = get_errors(data)
+     if errors:
+         captcha = errors.get('captcha')
+         if captcha:
+             raise HumbleCaptchaException(error_msg, request=response.request, response=response,
+-                                         captcha_required=captcha_required, authy_required=authy_required)
++                                         captcha_required=captcha_required, authy_required=authy_required,
++                                         captcha_skip_code=captcha_skip_code)
+ 
+         username = errors.get('username')
+         if username:
+             raise HumbleCredentialException(error_msg, request=response.request, response=response,
+-                                            captcha_required=captcha_required, authy_required=authy_required)
++                                            captcha_required=captcha_required, authy_required=authy_required,
++                                            captcha_skip_code=captcha_skip_code)
+ 
+         authy_token = errors.get("authy-token")
+         if authy_token:
+             raise HumbleTwoFactorException(error_msg, request=response.request, response=response,
+-                                           captcha_required=captcha_required, authy_required=authy_required)
++                                           captcha_required=captcha_required, authy_required=authy_required,
++                                           captcha_skip_code=captcha_skip_code)
+ 
+     raise HumbleAuthenticationException(error_msg, request=response.request, response=response,
+-                                        captcha_required=captcha_required, authy_required=authy_required)
++                                        captcha_required=captcha_required, authy_required=authy_required,
++                                        captcha_skip_code=captcha_skip_code)
+ 
+ 
+ def gamekeys_handler(client, response):
diff --git a/pkgs/games/humblebundle/fez.nix b/pkgs/games/humblebundle/fez.nix
new file mode 100644
index 00000000..5f23b97c
--- /dev/null
+++ b/pkgs/games/humblebundle/fez.nix
@@ -0,0 +1,35 @@
+{ stdenv, fetchHumbleBundle, unzip, mono, openal, SDL2 }:
+
+let
+  version = "1.0.2";
+  usVersion = stdenv.lib.replaceChars ["."] ["_"] version;
+in stdenv.mkDerivation rec {
+  name = "fez-${version}";
+  version = "09152013";
+
+  src = fetchHumbleBundle {
+    name = "${name}-bin";
+    md5 = "4ac954101835311f3528f5369e1fecb7";
+  };
+
+  unpackPhase = ''
+    ${unzip}/bin/unzip -qq "$src" 'data/*' || true
+    sourceRoot=data
+  '';
+
+  dontStrip = true;
+
+  buildPhase = ''
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${stdenv.lib.makeLibraryPath [ mono openal SDL2 ]}" \
+      FEZ.bin.x86_64
+  '';
+
+  installPhase = ''
+    ensureDir "$out/bin" "$out/libexec/fez/mono/2.0"
+    install -vD FEZ.bin.x86_64 "$out/libexec/fez/fez"
+    install -vt "$out/libexec/fez/mono/2.0" *.dll
+    ln -s "$out/libexec/fez/fez" "$out/bin/fez"
+  '';
+}
diff --git a/pkgs/games/humblebundle/ftl.nix b/pkgs/games/humblebundle/ftl.nix
new file mode 100644
index 00000000..5fd20a9f
--- /dev/null
+++ b/pkgs/games/humblebundle/ftl.nix
@@ -0,0 +1,38 @@
+{ stdenv, fetchHumbleBundle, makeWrapper, SDL, libGL, libdevil, freetype }:
+
+stdenv.mkDerivation rec {
+  name = "ftl-${version}";
+  version = "1.5.13";
+
+  src = fetchHumbleBundle {
+    machineName = "ftlfasterthanlight_soundtrack_linux";
+    downloadName = ".tar.gz";
+    suffix = "tar.gz";
+    md5 = "791e0bc8de73fcdcd5f461a4548ea2d8";
+  };
+
+  buildInputs = [ makeWrapper ];
+
+  patchPhase = let
+    rpath = stdenv.lib.makeLibraryPath [
+      SDL "$out" stdenv.cc.cc libGL libdevil freetype
+    ];
+  in ''
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}" data/amd64/bin/FTL
+  '';
+
+  installPhase = ''
+    install -vD "data/amd64/bin/FTL" "$out/libexec/ftl/FTL"
+    install -vD "data/amd64/lib/libbass.so" "$out/lib/libbass.so"
+    install -vD "data/amd64/lib/libbassmix.so" "$out/lib/libbassmix.so"
+
+    mkdir -p "$out/bin" "$out/share/ftl"
+    cp -vrt "$out/share/ftl" data/resources
+    makeWrapper "$out/libexec/ftl/FTL" "$out/bin/ftl" \
+      --run "cd '$out/share/ftl'"
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/grim-fandango.nix b/pkgs/games/humblebundle/grim-fandango.nix
new file mode 100644
index 00000000..00c1a036
--- /dev/null
+++ b/pkgs/games/humblebundle/grim-fandango.nix
@@ -0,0 +1,177 @@
+{ stdenv, fetchHumbleBundle, libGL, libpulseaudio, alsaLib, SDL2, writeText
+, xorg
+}:
+
+stdenv.mkDerivation rec {
+  name = "grim-fandango-remastered-${version}";
+  version = "1.4.0";
+
+  src = fetchHumbleBundle {
+    machineName = "grimfandango_linux";
+    suffix = "tar.gz";
+    md5 = "52e0590850102a1ae0db907bef413e57";
+  };
+
+  preloader = writeText "grim-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+    #include <dirent.h>
+    #include <fcntl.h>
+    #include <unistd.h>
+    #include <stdlib.h>
+    #include <stdio.h>
+    #include <string.h>
+    #include <sys/stat.h>
+    #include <sys/types.h>
+
+    int chdir(const char *path) {
+      static int (*_chdir) (const char *) = NULL;
+      if (_chdir == NULL) {
+        _chdir = dlsym(RTLD_NEXT, "chdir");
+        return _chdir(GRIM_DATADIR);
+      }
+      return _chdir(path);
+    }
+
+    #define CONCAT_ENV(path) \
+      if (asprintf(&result, "%s/%s", env, path) == -1) { \
+        perror("asprintf"); \
+        exit(1); \
+      }
+
+    static char *getSaveDir(void) {
+      const char *env;
+      static char *result = NULL;
+
+      if (result == NULL) {
+        if ((env = getenv("XDG_DATA_HOME")) != NULL) {
+          CONCAT_ENV("grim-fandango");
+        } else if ((env = getenv("HOME")) != NULL) {
+          CONCAT_ENV(".local/share/grim-fandango");
+        } else {
+          fputs("Unable to determine XDG_DATA_HOME or HOME.\n", stderr);
+          exit(1);
+        }
+      }
+
+      return result;
+    }
+
+    static void makedirs(char *path)
+    {
+      int pathlen = strlen(path);
+
+      static int (*_mkdir) (const char *, mode_t) = NULL;
+      if (_mkdir == NULL) _mkdir = dlsym(RTLD_NEXT, "mkdir");
+
+      for (int i = 1; i < pathlen; ++i) {
+        if (path[i] == '/') {
+          path[i] = '\0';
+          _mkdir(path, 0777);
+          path[i] = '/';
+        }
+      }
+    }
+
+    static char *mkSavePath(const char *path)
+    {
+      int savelen, pathlen;
+      char *buf, *savedir;
+
+      savedir = getSaveDir();
+      savelen = strlen(savedir);
+      pathlen = strlen(path);
+      buf = malloc(savelen + pathlen + 1);
+      strncpy(buf, savedir, savelen);
+      strncpy(buf + savelen, path, pathlen + 1);
+
+      return buf;
+    }
+
+    int mkdir(const char *pathname, mode_t mode) {
+      return 0;
+    }
+
+    FILE *fopen64(const char *path, const char *mode) {
+      FILE *fp;
+      char *buf;
+
+      static FILE *(*_fopen) (const char *, const char *) = NULL;
+      if (_fopen == NULL) _fopen = dlsym(RTLD_NEXT, "fopen64");
+
+      if (strncmp(path, "./Saves/", 8) == 0) {
+        path += 7;
+        buf = mkSavePath(path);
+        if ((fp = _fopen(buf, mode)) == NULL) {
+          makedirs(buf);
+          fp = _fopen(buf, mode);
+        }
+        free(buf);
+        return fp;
+      }
+
+      return _fopen(path, mode);
+    }
+
+    DIR *opendir(const char *name) {
+      DIR *dp;
+      char *buf;
+
+      static DIR *(*_opendir) (const char *) = NULL;
+      if (_opendir == NULL) _opendir = dlsym(RTLD_NEXT, "opendir");
+
+      if (strncmp(name, "./Saves/", 8) == 0) {
+        name += 7;
+        buf = mkSavePath(name);
+        if ((dp = _opendir(buf)) == NULL) {
+          makedirs(buf);
+          dp = _opendir(buf);
+        }
+        free(buf);
+        return dp;
+      }
+
+      return _opendir(name);
+    }
+  '';
+
+  rpath = stdenv.lib.makeLibraryPath [
+    libGL stdenv.cc.cc libpulseaudio alsaLib.out SDL2 xorg.libX11
+  ];
+
+  buildPhase = ''
+    cc -Werror -Wall -std=gnu11 -shared "$preloader" -o preload.so -fPIC \
+      -DGRIM_DATADIR="\"$out/share/grim-fandango\""
+    patchelf --set-rpath "$rpath" bin/libchore.so
+    patchelf --set-rpath "$rpath" bin/libLua.so
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --add-needed "$out/libexec/grim-fandango/libgrim-preload.so" \
+      bin/GrimFandango
+    patchelf --replace-needed libSDL2-2.0.so.1 libSDL2.so bin/GrimFandango
+    patchelf --set-rpath "$out/libexec/grim-fandango:$rpath" bin/GrimFandango
+  '';
+
+  installPhase = ''
+    install -vsD preload.so "$out/libexec/grim-fandango/libgrim-preload.so"
+    install -vD bin/libchore.so "$out/libexec/grim-fandango/libchore.so"
+    install -vD bin/libLua.so "$out/libexec/grim-fandango/libLua.so"
+    install -vD bin/GrimFandango "$out/bin/grim-fandango"
+    if ldd "$out/bin/grim-fandango" | grep -F 'not found'; then exit 1; fi
+
+    mkdir -p "$out/share/grim-fandango"
+    find bin -maxdepth 1 -mindepth 1 \
+      \( -path bin/i386 \
+      -o -path bin/amd64 \
+      -o -path bin/common-licenses \
+      -o -path bin/scripts \
+      -o -path bin/GrimFandango \
+      -o -name '*.so' \
+      -o -name '*.so.*' \
+      -o -name '*.txt' \
+      \) -prune -o -print | xargs cp -vrt "$out/share/grim-fandango"
+  '';
+
+  dontStrip = true;
+  dontPatchELF = true;
+}
diff --git a/pkgs/games/humblebundle/guacamelee.nix b/pkgs/games/humblebundle/guacamelee.nix
new file mode 100644
index 00000000..5cc39837
--- /dev/null
+++ b/pkgs/games/humblebundle/guacamelee.nix
@@ -0,0 +1,61 @@
+{ stdenv, fetchHumbleBundle, unzip, SDL2, libGL, writeText, makeWrapper }:
+
+stdenv.mkDerivation rec {
+  name = "guacamelee-${version}";
+  version = "1393037377";
+
+  src = fetchHumbleBundle {
+    machineName = "guacamelee_goldedition_linux";
+    suffix = "sh";
+    md5 = "b06af932c1aaefb8f157c977061388ef";
+  };
+
+  unpackCmd = ''
+    ${unzip}/bin/unzip "$src" 'data/*' || :
+  '';
+
+  preloader = writeText "guacamelee-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+
+    int chdir(const char *path) {
+      int (*_chdir) (const char *) = dlsym(RTLD_NEXT, "chdir");
+      return _chdir(DATA);
+    }
+  '';
+
+  buildInputs = [ makeWrapper ];
+
+  buildPhase = let
+    rpath = stdenv.lib.makeLibraryPath [ SDL2 stdenv.cc.cc libGL ];
+    fmodRpath = stdenv.lib.makeLibraryPath [ stdenv.cc.cc ];
+  in ''
+    gcc -Werror -shared "$preloader" -o preloader.so -ldl \
+      -DDATA=\"$out/share/guacamelee\"
+
+    for i in libfmodevent-4.44.27.so libfmodex-4.44.27.so; do
+      patchelf --set-rpath "${fmodRpath}:$out/libexec/guacamelee" \
+        "x86/lib32/$i"
+    done
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}:$out/libexec/guacamelee" x86/game-bin
+  '';
+
+  installPhase = ''
+    install -vD x86/game-bin "$out/libexec/guacamelee/guacamelee"
+    install -vD preloader.so "$out/libexec/guacamelee/preloader.so"
+
+    makeWrapper "$out/libexec/guacamelee/guacamelee" "$out/bin/guacamelee" \
+      --set LD_PRELOAD "$out/libexec/guacamelee/preloader.so"
+
+    for i in libfmodevent-4.44.27.so libfmodex-4.44.27.so; do
+      install -vD "x86/lib32/$i" "$out/libexec/guacamelee/$i"
+    done
+
+    mkdir -p "$out/share"
+    cp -vRd noarch "$out/share/guacamelee"
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/hammerwatch.nix b/pkgs/games/humblebundle/hammerwatch.nix
new file mode 100644
index 00000000..ab5bc29f
--- /dev/null
+++ b/pkgs/games/humblebundle/hammerwatch.nix
@@ -0,0 +1,40 @@
+{ stdenv, fetchHumbleBundle, makeWrapper, unzip, mono, SDL2, libGL, openal
+, pulseaudio
+}:
+
+# FIXME: Dosn't support the XDG Base Directory Specification,
+#        so enforce it using LD_PRELOAD maybe?
+
+stdenv.mkDerivation rec {
+  name = "hammerwatch-${version}";
+  version = "1.3";
+
+  src = fetchHumbleBundle {
+    machineName = "hammerwatch_linux";
+    suffix = "zip";
+    md5 = "7cd77e4395f394c3062322c96e418732";
+  };
+
+  buildInputs = [ unzip makeWrapper ];
+
+  installPhase = let
+    rpath = stdenv.lib.makeLibraryPath [ SDL2 libGL openal pulseaudio ];
+    monoNoLLVM = mono.override { withLLVM = false; };
+  in ''
+    mkdir -p "$out/lib"
+    cp -rt "$out/lib" SDL2-CS.dll SDL2-CS.dll.config \
+      TiltedEngine.dll Lidgren.Network.dll FarseerPhysicsOTK.dll \
+      ICSharpCode.SharpZipLib.dll SteamworksManaged.dll NVorbis.dll
+
+    libexec="$out/libexec/hammerwatch"
+    install -vD Hammerwatch.exe "$libexec/hammerwatch.exe"
+    cp -rt "$libexec" assets.bin editor levels
+
+    makeWrapper "${monoNoLLVM}/bin/mono" "$out/bin/hammerwatch" \
+      --add-flags "$libexec/hammerwatch.exe" \
+      --set MONO_PATH "$out/lib" \
+      --set LD_LIBRARY_PATH "${rpath}"
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/jamestown.nix b/pkgs/games/humblebundle/jamestown.nix
new file mode 100644
index 00000000..15900bba
--- /dev/null
+++ b/pkgs/games/humblebundle/jamestown.nix
@@ -0,0 +1,55 @@
+{ stdenv, fetchHumbleBundle, unzip, pkgsi686Linux, expect, makeWrapper
+, SDL, openal
+}:
+
+let
+  version = "1.0.2";
+  usVersion = stdenv.lib.replaceChars ["."] ["_"] version;
+in stdenv.mkDerivation rec {
+  name = "jamestown-${version}";
+
+  src = fetchHumbleBundle {
+    machineName = "jamestown_linux";
+    downloadName = ".zip";
+    suffix = "zip";
+    md5 = "dcfb4348aba89f0f26bf5b4c7e05d936";
+  };
+
+  buildInputs = [ makeWrapper ];
+
+  unpackPhase = ''
+    ${unzip}/bin/unzip -q "$src"
+    patchelf --set-interpreter "${pkgsi686Linux.glibc}"/lib/ld-linux.so.* \
+      "JamestownInstaller_${usVersion}-bin"
+    ${expect}/bin/expect <<INSTALL
+    spawn "./JamestownInstaller_${usVersion}-bin"
+    expect "see more?"
+    send "n\r"
+    expect "Accept this license?"
+    send "y\r"
+    expect "Press enter to continue."
+    send "\r"
+    expect "Enter path"
+    send "$(pwd)/${name}\r"
+    expect eof
+    INSTALL
+    sourceRoot="$(pwd)/${name}"
+  '';
+
+  installPhase = let
+    rpath = stdenv.lib.makeLibraryPath [ SDL openal ];
+  in ''
+    libexec="$out/libexec/jamestown"
+    install -vD Jamestown-amd64 "$libexec/jamestown"
+
+    mkdir -p "$out/share"
+    mv Archives "$out/share/jamestown"
+
+    makeWrapper "$(cat "$NIX_CC/nix-support/dynamic-linker")" \
+      "$out/bin/jamestown" \
+      --add-flags "$libexec/jamestown" \
+      --set LD_LIBRARY_PATH "${rpath}"
+
+    false # Both amd64 and i686 binaries are fucking BROKEN, wait for 1.0.3...
+  '';
+}
diff --git a/pkgs/games/humblebundle/liads.nix b/pkgs/games/humblebundle/liads.nix
new file mode 100644
index 00000000..975d076d
--- /dev/null
+++ b/pkgs/games/humblebundle/liads.nix
@@ -0,0 +1,16 @@
+{ buildUnity, fetchHumbleBundle }:
+
+buildUnity {
+  name = "liads";
+  version = "20180427";
+
+  fullName = "LoversInADangerousSpacetime";
+  saveDir = "AsteroidBase/LoversInADangerousSpacetime";
+  sandbox.paths.required = [ "$XDG_DATA_HOME/LoversInADangerousSpacetime" ];
+
+  src = fetchHumbleBundle {
+    machineName = "loversinadangerousspacetime_linux";
+    suffix = "zip";
+    md5 = "67b6bc5ba5590fb50e95996b267f8c60";
+  };
+}
diff --git a/pkgs/games/humblebundle/megabytepunch.nix b/pkgs/games/humblebundle/megabytepunch.nix
new file mode 100644
index 00000000..04f715cf
--- /dev/null
+++ b/pkgs/games/humblebundle/megabytepunch.nix
@@ -0,0 +1,14 @@
+{ buildUnity, fetchHumbleBundle }:
+
+buildUnity {
+  name = "megabytepunch";
+  fullName = "MegabytePunch";
+  saveDir = "Reptile/Megabyte Punch";
+  version = "1.12";
+
+  src = fetchHumbleBundle {
+    machineName = "megabytepunch_linux";
+    suffix = "tar.gz";
+    md5 = "13487ae35c99817ce5f19b45fa51158b";
+  };
+}
diff --git a/pkgs/games/humblebundle/minimetro.nix b/pkgs/games/humblebundle/minimetro.nix
new file mode 100644
index 00000000..a5c48775
--- /dev/null
+++ b/pkgs/games/humblebundle/minimetro.nix
@@ -0,0 +1,15 @@
+{ buildUnity, fetchHumbleBundle }:
+
+buildUnity rec {
+  name = "minimetro";
+  version = "39";
+  fullName = "Mini Metro";
+  saveDir = "Dinosaur Polo Club/Mini Metro";
+
+  src = fetchHumbleBundle {
+    name = "MiniMetro-release-39-linux.tar.gz";
+    machineName = "minimetro_linux";
+    downloadName = "Download";
+    md5 = "3e7afefbcc68b6295821394e31f5e48b";
+  };
+}
diff --git a/pkgs/games/humblebundle/opus-magnum.nix b/pkgs/games/humblebundle/opus-magnum.nix
new file mode 100644
index 00000000..07845a68
--- /dev/null
+++ b/pkgs/games/humblebundle/opus-magnum.nix
@@ -0,0 +1,43 @@
+{ stdenv, lib, buildGame, fetchHumbleBundle, makeWrapper, mono
+, SDL2, SDL2_image, SDL2_mixer, libvorbis
+}:
+
+buildGame rec {
+  name = "opus-magnum-${version}";
+  version = "20180111";
+
+  src = fetchHumbleBundle {
+    machineName = "opus_magnum_4vffp_linux_Z9zWf";
+    suffix = "zip";
+    md5 = "8bfc49528b69630f8df983fd545518bb";
+  };
+
+  arch = if stdenv.system == "x86_64-linux" then "x86_64" else "x86";
+
+  nativeBuildInputs = [ makeWrapper ];
+
+  buildPhase = let
+    dllmap = {
+      SDL2 = "${SDL2}/lib/libSDL2.so";
+      SDL2_image = "${SDL2_image}/lib/libSDL2_image.so";
+      SDL2_mixer = "${SDL2_mixer}/lib/libSDL2_mixer.so";
+      libvorbisfile-3 = "${libvorbis}/lib/libvorbisfile.so";
+    };
+  in lib.concatStrings (lib.mapAttrsToList (dll: target: ''
+    sed -i -e '/<dllmap.*dll="${dll}\.dll".*os="linux"/ {
+      s!target="[^"]*"!target="${target}"!
+    }' Lightning.exe.config
+  '') dllmap);
+
+  installPhase = ''
+    mkdir -p "$out/bin" "$out/share/opus-magnum" "$out/libexec/opus-magnum"
+    cp -rvt "$out/share/opus-magnum" Content PackedContent
+    cp -rvt "$out/libexec/opus-magnum" Lightning.exe* Ionic.Zip.Reduced.dll
+
+    makeWrapper ${lib.escapeShellArg mono}/bin/mono "$out/bin/opus-magnum" \
+      --add-flags "$out/libexec/opus-magnum/Lightning.exe" \
+      --run "cd '$out/share/opus-magnum'"
+  '';
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/Opus Magnum" "$HOME/Desktop" ];
+}
diff --git a/pkgs/games/humblebundle/owlboy.nix b/pkgs/games/humblebundle/owlboy.nix
new file mode 100644
index 00000000..3324ce43
--- /dev/null
+++ b/pkgs/games/humblebundle/owlboy.nix
@@ -0,0 +1,102 @@
+{ stdenv, lib, buildGame, fetchHumbleBundle, unzip, makeWrapper, mono
+, SDL2, SDL2_image, openal, libvorbis
+, writeText
+}:
+
+buildGame rec {
+  name = "owlboy-${version}";
+  version = "20171229";
+
+  src = fetchHumbleBundle {
+    suffix = "bin";
+    machineName = "owlboy_linux";
+    md5 = "c2e99502013c7d2529bc2aefb6416dcf";
+  };
+
+  unpackCmd = "${unzip}/bin/unzip -qq \"$curSrc\" 'data/*' || :";
+
+  nativeBuildInputs = [ makeWrapper ];
+
+  libdir = if stdenv.system == "x86_64-linux" then "lib64" else "lib";
+
+  prePatch = ''
+    find -type f -name '*.ini' -exec sed -i -e 's/${"\r"}$//' {} +
+  '';
+
+  buildPhase = let
+    dllmap = {
+      SDL2 = "${SDL2}/lib/libSDL2.so";
+      SDL2_image = "${SDL2_image}/lib/libSDL2_image.so";
+      soft_oal = "${openal}/lib/libopenal.so";
+      libvorbisfile-3 = "${libvorbis}/lib/libvorbisfile.so";
+      MojoShader = "$out/lib/owlboy/libmojoshader.so";
+    };
+  in ''
+    cc -Werror -shared "$preloader" -o preloader.so -ldl -fPIC \
+      -DSTOREPATH=\"$out\"
+  '' + lib.concatStrings (lib.mapAttrsToList (dll: target: ''
+    sed -i -e '/<dllmap.*dll="${dll}\.dll".*os="linux"/ {
+      s!target="[^"]*"!target="'"${target}"'"!
+    }' FNA.dll.config
+  '') dllmap);
+
+  # The game tries to open data files in read-write mode, so use LD_PRELOAD to
+  # avoid this whenever a store path is involved.
+  preloader = writeText "owlboy-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+    #include <fcntl.h>
+    #include <stdarg.h>
+    #include <stdio.h>
+    #include <stdbool.h>
+    #include <string.h>
+    #include <sys/stat.h>
+
+    static bool isDataPath(const char *path) {
+      return strncmp(path, "content/", 8) == 0
+          || strncmp(path, STOREPATH, sizeof(STOREPATH) - 1) == 0;
+    }
+
+    int access(const char *path, int mode) {
+      static int (*_access) (const char *, int) = NULL;
+      if (_access == NULL) _access = dlsym(RTLD_NEXT, "access");
+      if (isDataPath(path) && mode & W_OK)
+        mode = mode & ~W_OK | R_OK;
+      return _access(path, mode);
+    }
+
+    int open(const char *path, int flags, ...) {
+      va_list ap;
+      mode_t mode;
+      static int (*_open) (const char *, int, mode_t) = NULL;
+      if (_open == NULL) _open = dlsym(RTLD_NEXT, "open");
+      va_start(ap, flags);
+      mode = va_arg(ap, mode_t);
+      va_end(ap);
+      if (isDataPath(path) && flags & O_RDWR)
+        flags = flags & ~O_RDWR | O_RDONLY;
+      return _open(path, flags, mode);
+    }
+  '';
+
+  installPhase = ''
+    mkdir -p "$out/bin" "$out/share" "$out/libexec/owlboy"
+    install -vD preloader.so "$out/lib/owlboy/preloader.so"
+    cp -rv content "$out/share/owlboy"
+    cp -rv monoconfig "$out/libexec/owlboy/Owlboy.exe.config"
+    cp -rvt "$out/libexec/owlboy" Owlboy.exe \
+      FNA.dll* GamedevUtility.dll MoonSharp.Interpreter.dll TimSort.dll
+    cp -rvt "$out/lib/owlboy" "$libdir/libmojoshader.so"
+    ln -vs "$out/share/owlboy" "$out/libexec/owlboy/content"
+
+    makeWrapper ${lib.escapeShellArg mono}/bin/mono \
+      "$out/bin/owlboy" \
+      --set LD_PRELOAD "$out/lib/owlboy/preloader.so" \
+      --add-flags "$out/libexec/owlboy/Owlboy.exe" \
+      --run "cd '$out/libexec/owlboy'"
+  '';
+
+  sandbox.paths.required = [
+    "$XDG_DATA_HOME/Owlboy" "$XDG_CONFIG_HOME/Owlboy"
+  ];
+}
diff --git a/pkgs/games/humblebundle/pico-8.nix b/pkgs/games/humblebundle/pico-8.nix
new file mode 100644
index 00000000..b5dbfefc
--- /dev/null
+++ b/pkgs/games/humblebundle/pico-8.nix
@@ -0,0 +1,51 @@
+{ stdenv, fetchHumbleBundle, SDL2, unzip, xorg, libudev, alsaLib, dbus
+, libpulseaudio, libdrm, libvorbis, json_c }:
+
+stdenv.mkDerivation rec {
+  name = "pico-8-${version}";
+  version = "0.1.12c";
+
+  src = fetchHumbleBundle {
+    name = "pico8_linux";
+    machineName = "pico8_linux";
+    downloadName = {
+      "x86_64-linux" = "64-bit";
+      "i686-linux"   = "32-bit";
+    }.${stdenv.system};
+    md5 = {
+      "x86_64-linux" = "8d4fbe66ceb1528987841a6743f132db";
+      "i686-linux"   = "1e8633fb52c18e803ff7eebe6ddc76f9";
+    }.${stdenv.system};
+  };
+
+  unpackCmd = ''
+    ${unzip}/bin/unzip -qq -d . "$src" || :
+  '';
+
+  phases = [ "unpackPhase" "buildPhase" "installPhase" ];
+
+  buildPhase = let
+    rpath = stdenv.lib.makeLibraryPath [
+      stdenv.cc.cc SDL2 xorg.libXxf86vm xorg.libXcursor xorg.libXi
+      xorg.libXrandr libudev alsaLib dbus
+      libpulseaudio libdrm libvorbis json_c
+    ];
+  in ''
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}" pico8
+  '';
+
+  installPhase = ''
+    install -vD pico8 "$out/share/pico8"
+    install -vD pico8.dat "$out/share/pico8.dat"
+    install -vD pico-8.txt "$out/share/pico-8.txt"
+    install -vD license.txt "$out/share/license.txt"
+    install -vD lexaloffle-pico8.png "$out/share/lexaloffle-pico8.png"
+
+    mkdir -p "$out/bin"
+    ln -s "$out/share/pico8" "$out/bin/pico8"
+  '';
+
+  dontPatchELF = true;
+}
diff --git a/pkgs/games/humblebundle/rocketbirds.nix b/pkgs/games/humblebundle/rocketbirds.nix
new file mode 100644
index 00000000..a57d1562
--- /dev/null
+++ b/pkgs/games/humblebundle/rocketbirds.nix
@@ -0,0 +1,11 @@
+{ stdenv, fetchHumbleBundle }:
+
+stdenv.mkDerivation rec {
+  name = "rocketbirds-${version}";
+  version = "20130917";
+
+  src = fetchHumbleBundle {
+    name = "Rocketbirds${version}.sh";
+    md5 = "7c5e6da4cd7fc7f2f51861f8b96a386f";
+  };
+}
diff --git a/pkgs/games/humblebundle/spaz.nix b/pkgs/games/humblebundle/spaz.nix
new file mode 100644
index 00000000..24d32e53
--- /dev/null
+++ b/pkgs/games/humblebundle/spaz.nix
@@ -0,0 +1,39 @@
+{ stdenv, fetchHumbleBundle, unzip, pkgsi686Linux }:
+
+stdenv.mkDerivation rec {
+  name = "spaz-${version}";
+  version = "09182012";
+
+  src = fetchHumbleBundle {
+    name = "spaz-linux-humblebundle-${version}-bin";
+    md5 = "9b2f28009949f2dff9f3a737e46fabfd";
+  };
+
+  buildInputs = [ pkgsi686Linux.makeWrapper ];
+
+  unpackCmd = ''
+    ${unzip}/bin/unzip -qq "$src" 'data/*' || true
+  '';
+
+  dontStrip = true;
+
+  buildPhase = let
+    libs = pkgsi686Linux.stdenv.lib.makeLibraryPath [
+      pkgsi686Linux.stdenv.cc.cc pkgsi686Linux.SDL
+    ];
+  in ''
+    patchelf --set-interpreter "${pkgsi686Linux.glibc}"/lib/ld-linux.so.* \
+             --set-rpath "${libs}" SPAZ
+  '';
+
+  installPhase = let
+    libs = pkgsi686Linux.stdenv.lib.makeLibraryPath [
+      pkgsi686Linux.libGL pkgsi686Linux.openal pkgsi686Linux.alsaPlugins
+    ];
+  in ''
+    install -vD SPAZ "$out/libexec/spaz/spaz"
+    cp -rt "$out/libexec/spaz" audio.so common game mods
+    makeWrapper "$out/libexec/spaz/spaz" "$out/bin/spaz" \
+      --set LD_LIBRARY_PATH "${libs}"
+  '';
+}
diff --git a/pkgs/games/humblebundle/starbound.nix b/pkgs/games/humblebundle/starbound.nix
new file mode 100644
index 00000000..d3c11935
--- /dev/null
+++ b/pkgs/games/humblebundle/starbound.nix
@@ -0,0 +1,328 @@
+{ stdenv, fetchHumbleBundle, unzip, fetchurl, writeText, SDL2, libGLU_combined
+, xorg, makeDesktopItem
+}:
+
+let
+  binaryDeps = {
+    starbound.deps = [
+      SDL2 libGLU_combined xorg.libX11 xorg.libICE xorg.libSM xorg.libXext
+    ];
+    starbound.needsBootconfig = true;
+
+    starbound_server.name = "starbound-server";
+    starbound_server.needsBootconfig = true;
+
+    asset_packer.name = "starbound-asset-packer";
+    asset_unpacker.name = "starbound-asset-unpacker";
+
+    dump_versioned_json.name = "starbound-dump-versioned-json";
+    make_versioned_json.name = "starbound-make-versioned-json";
+
+    planet_mapgen.name = "starbound-planet-mapgen";
+  };
+
+  desktopItem = makeDesktopItem {
+    name = "starbound";
+    exec = "starbound";
+    icon = fetchurl {
+      url = "http://i1305.photobucket.com/albums/s544/ClockworkBarber/"
+          + "logo_zps64c4860d.png";
+      sha256 = "11fiiy0vcxzix1j81732cjh16wi48k4vag040vmbhad50ps3mg0q";
+    };
+    comment = "An extraterrestrial sandbox adventure game";
+    desktopName = "Starbound";
+    genericName = "starbound";
+    categories = "Game;";
+  };
+
+  patchBinary = bin: attrs: ''
+    mkdir -p "patched/$(dirname "${bin}")"
+    cp -t "patched/$(dirname "${bin}")" "linux/${bin}"
+    chmod +x "patched/$(basename "${bin}")"
+    ${stdenv.lib.optionalString (attrs.needsBootconfig or false) ''
+      for offset in $(
+        grep -abz '^sbinit\.config''$' "patched/$(basename "${bin}")" \
+          | cut -d: -f1 -z | xargs -0
+      ); do
+        for i in $(seq 13); do printf '\x07'; done \
+          | dd of="patched/$(basename "${bin}")" \
+               obs=1 seek="$offset" \
+               count=13 conv=notrunc
+      done
+      patchelf \
+        --add-needed "$lib/lib/libstarbound-preload.so" \
+        "patched/$(basename "${bin}")"
+    ''}
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${stdenv.lib.makeLibraryPath (attrs.deps or [])}" \
+      "patched/$(basename "${bin}")"
+    if ldd "patched/$(basename "${bin}")" | grep -F 'not found' \
+       | grep -v 'libstarbound-preload\.so\|libsteam_api\.so'; then
+      exit 1;
+    fi
+  '';
+
+  preloaderSource = writeText "starbound-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+    #include <fcntl.h>
+    #include <malloc.h>
+    #include <stdarg.h>
+    #include <stdio.h>
+    #include <stdlib.h>
+    #include <string.h>
+    #include <sys/stat.h>
+    #include <sys/wait.h>
+    #include <unistd.h>
+
+    #define MAGIC "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07"
+
+    static char *getXdgDataHome(void) {
+      int envlen;
+      char *result;
+      const char *env;
+
+      if ((env = getenv("XDG_DATA_HOME")) != NULL)
+        return strdup(env);
+      if ((env = getenv("HOME")) == NULL)
+        return NULL;
+
+      envlen = strlen(env);
+      if ((result = malloc(envlen + 14)) == NULL)
+        return NULL;
+      strncpy(result, env, envlen);
+      strncpy(result + envlen, "/.local/share", 14);
+      return result;
+    }
+
+    static char *mkJsonString(const char *str) {
+      char *result, *out;
+
+      if ((result = malloc(strlen(str) * 6 + 3)) == NULL)
+        return NULL;
+
+      out = result;
+      *out++ = '"';
+
+      for (; *str != '\0'; str++) {
+        switch (*str) {
+          case '"':  *out++ = '\\'; *out++ = '"'; break;
+          case '\\': *out++ = '\\'; *out++ = '\\'; break;
+          case '\b': *out++ = '\\'; *out++ = 'b'; break;
+          case '\f': *out++ = '\\'; *out++ = 'f'; break;
+          case '\n': *out++ = '\\'; *out++ = 'n'; break;
+          case '\r': *out++ = '\\'; *out++ = 'r'; break;
+          case '\t': *out++ = '\\'; *out++ = 't'; break;
+          default:
+            if (*str >= 0 && *str <= 31) {
+              *out++ = '\\';
+              *out++ = 'u';
+              snprintf(out, 5, "%04x", *str);
+              out += 4;
+            } else {
+              *out++ = *str;
+            }
+            break;
+        }
+      }
+
+      *out++ = '"';
+      *out = 0;
+
+      return result;
+    }
+
+    static char *mkDataDir(const char *dataHome, const char *append) {
+      char *buf, *out;
+      int homeLen = strlen(dataHome);
+      int appendLen = strlen(append);
+
+      if ((buf = malloc(homeLen + appendLen + 2)) == NULL)
+        return NULL;
+
+      snprintf(buf, homeLen + appendLen + 2, "%s/%s", dataHome, append);
+
+      out = mkJsonString(buf);
+      free(buf);
+
+      return out;
+    }
+
+    static int makedirs(const char *path) {
+      char *buf, *p;
+
+      if (strlen(path) == 0)
+        return 1;
+
+      if ((buf = strdup(path)) == NULL)
+        return 1;
+
+      for (p = buf + 1; *p != '\0'; p++) {
+        if (*p != '/') continue;
+        *p = '\0';
+        mkdir(buf, 0777);
+        *p = '/';
+      }
+
+      free(buf);
+      return 0;
+    }
+
+    static int writeSBInit(FILE *sbinit) {
+      char *buf, *dataHome;
+      int homeLen, ret;
+
+      if ((dataHome = getXdgDataHome()) == NULL)
+        return 1;
+
+      homeLen = strlen(dataHome);
+      if ((buf = malloc(homeLen + 12)) == NULL)
+        goto errout;
+      strncpy(buf, dataHome, homeLen);
+      strncpy(buf + homeLen, "/starbound/", 12);
+      ret = makedirs(buf);
+      free(buf);
+      if (ret != 0) goto errout;
+
+      fputs("{\"assetDirectories\":[", sbinit);
+
+      if ((buf = mkJsonString(STARBOUND_ASSET_DIR)) == NULL)
+        goto errout;
+      fputs(buf, sbinit);
+      free(buf);
+
+      fputc(',', sbinit);
+
+      if ((buf = mkDataDir(dataHome, "starbound/mods/")) == NULL)
+        goto errout;
+      fputs(buf, sbinit);
+      free(buf);
+
+      fputs("],\"storageDirectory\":", sbinit);
+
+      if ((buf = mkDataDir(dataHome, "starbound/")) == NULL)
+        goto errout;
+      fputs(buf, sbinit);
+      free(buf);
+
+      fputs("}", sbinit);
+      free(dataHome);
+      return 0;
+    errout:
+      free(dataHome);
+      return 1;
+    }
+
+    static FILE *fakeSBInitHandle = NULL;
+
+    static int fakeSBInit(void) {
+      fakeSBInitHandle = tmpfile();
+      if (writeSBInit(fakeSBInitHandle) != 0) {
+        fclose(fakeSBInitHandle);
+        fakeSBInitHandle = NULL;
+        return -1;
+      }
+      rewind(fakeSBInitHandle);
+      return fileno(fakeSBInitHandle);
+    }
+
+    int open(const char *path, int flags, ...) {
+      va_list ap;
+      mode_t mode;
+      static int (*_open) (const char *, int, mode_t) = NULL;
+
+      if (_open == NULL)
+        _open = dlsym(RTLD_NEXT, "open");
+
+      va_start(ap, flags);
+      mode = va_arg(ap, mode_t);
+      va_end(ap);
+
+      if (strncmp(path, MAGIC, 14) != 0)
+        return _open(path, flags, mode);
+
+      return fakeSBInit();
+    }
+
+    int close(int fd) {
+      int ret;
+      static int (*_close) (int) = NULL;
+
+      if (_close == NULL)
+        _close = dlsym(RTLD_NEXT, "close");
+
+      if (fakeSBInitHandle != NULL) {
+        ret = fclose(fakeSBInitHandle);
+        fakeSBInitHandle = NULL;
+      } else {
+        ret = _close(fd);
+      }
+
+      return ret;
+    }
+
+    int SteamAPI_Init(void) {
+      return 0;
+    }
+  '';
+
+in stdenv.mkDerivation rec {
+  name = "starbound-${version}";
+  version = "1.3.3";
+
+  src = fetchHumbleBundle {
+    name = "starbound-linux-${version}.zip";
+    machineName = "starbound_linux";
+    md5 = "91decc3a8fa9cd0be08952422f5adb39";
+  };
+
+  outputs = [ "out" "lib" "assets" ];
+
+  nativeBuildInputs = [ unzip ];
+
+  buildPhase = with stdenv.lib; ''
+    cc -Werror -shared "${preloaderSource}" -o preload.so -ldl -fPIC \
+      -DSTARBOUND_ASSET_DIR="\"$assets\""
+    ${concatStrings (mapAttrsToList patchBinary binaryDeps)}
+    patchelf --remove-needed libsteam_api.so patched/starbound
+  '';
+
+  doCheck = true;
+
+  checkPhase = ''
+    checkFailed=
+    for i in linux/*; do
+      [ -f "$i" ] || continue
+
+      case "$(basename "$i")" in
+        sbinit.config) continue;;
+        *.s[ho]) continue;;
+      esac
+
+      [ ! -e "patched/$(basename "$i")" ] || continue
+
+      echo "Found missing binary $i from the upstream tree."
+      checkFailed=1
+    done
+
+    [ -z "$checkFailed" ]
+  '';
+
+  installPhase = ''
+    install -vsD preload.so "$lib/lib/libstarbound-preload.so"
+
+    ${stdenv.lib.concatStrings (stdenv.lib.mapAttrsToList (bin: attrs: let
+      basename = builtins.baseNameOf bin;
+    in ''
+      install -vD "patched/${basename}" "$out/bin/${attrs.name or basename}"
+    '') binaryDeps)}
+
+    install -m 0644 -vD "${desktopItem}/share/applications/starbound.desktop" \
+      "$out/share/applications/starbound.desktop"
+
+    cp -vr assets "$assets"
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/swordsandsoldiers.nix b/pkgs/games/humblebundle/swordsandsoldiers.nix
new file mode 100644
index 00000000..755f001c
--- /dev/null
+++ b/pkgs/games/humblebundle/swordsandsoldiers.nix
@@ -0,0 +1,43 @@
+{ stdenv, fetchHumbleBundle, makeWrapper
+, SDL, libGL, zlib, openal, libvorbis, xorg, fontconfig, freetype, libogg
+}:
+
+stdenv.mkDerivation rec {
+  name = "swordsandsoldiers-${version}";
+  version = "20120325";
+
+  src = fetchHumbleBundle {
+    machineName = "swordsandsoldiers_android_and_pc_linux";
+    downloadName = "x86_64.tar.gz";
+    suffix = "tar.gz";
+    md5 = "5f0c9789fa053cbf6bac021a338245bb";
+  };
+
+  buildInputs = [ makeWrapper ];
+
+  patchPhase = let
+    rpath = stdenv.lib.makeLibraryPath [
+      SDL libGL zlib openal libvorbis fontconfig freetype stdenv.cc.cc libogg
+      xorg.libX11 xorg.libXft xorg.libXinerama xorg.libXext xorg.libXpm
+    ];
+  in ''
+    for i in SwordsAndSoldiers.bin SwordsAndSoldiersSetup.bin; do
+      patchelf \
+        --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+        --set-rpath "${rpath}" "$i"
+    done
+  '';
+
+  installPhase = ''
+    libexec="$out/libexec/swordsandsoldiers"
+    install -vD SwordsAndSoldiers.bin "$libexec/swordsandsoldiers"
+    install -vD SwordsAndSoldiersSetup.bin "$libexec/setup"
+    mv Data "$libexec/"
+
+    mkdir -p "$out/bin"
+    ln -s "$libexec/swordsandsoldiers" "$out/bin/swordsandsoldiers"
+    ln -s "$libexec/setup" "$out/bin/swordsandsoldiers-setup"
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/games/humblebundle/the-bridge.nix b/pkgs/games/humblebundle/the-bridge.nix
new file mode 100644
index 00000000..92e5b691
--- /dev/null
+++ b/pkgs/games/humblebundle/the-bridge.nix
@@ -0,0 +1,37 @@
+{ buildUnity, fetchHumbleBundle
+, libGLU
+}:
+
+buildUnity rec {
+  fullName = "TheBridge";
+  name = fullName;
+  version = "20140908"; # 1410197597, or 1410196636 (same date).
+
+  src = fetchHumbleBundle {
+    name = "TheBridgeLinux_1410196636.zip";
+    machineName = "thebridge_linux";
+    downloadName = "Download";
+    md5 = "6d3f5e7ff8d10d47f04ffabb8b9a031e";
+  };
+
+  buildInputs = [ libGLU ];
+
+  meta = {
+    homepage = [
+      http://thebridgeisblackandwhite.com
+      https://www.humblebundle.com/store/the-bridge
+    ];
+    #editor = "The Quantum Astrophysicists Guild";
+    description = "A 2D logic puzzle game that plays with physics and perspective";
+    longDescription = ''
+      The Bridge is a 2D logic puzzle game that forces the player to reevaluate
+      their preconceptions of physics and perspective. It is Isaac Newton meets
+      M. C. Escher. Manipulate gravity to redefine the ceiling as the floor
+      while venturing through impossible architectures. Explore increasingly
+      difficult worlds, each uniquely detailed and designed to leave the player
+      with a pronounced sense of intellectual accomplishment. The Bridge
+      exemplifies games as an art form, with beautifully hand-drawn art in the
+      style of a black-and-white lithograph.
+    '';
+  };
+}
diff --git a/pkgs/games/humblebundle/trine2.nix b/pkgs/games/humblebundle/trine2.nix
new file mode 100644
index 00000000..a6cef22d
--- /dev/null
+++ b/pkgs/games/humblebundle/trine2.nix
@@ -0,0 +1,97 @@
+{ buildGame, fetchHumbleBundle, makeWrapper, runCommandCC, writeText
+, coreutils, openal, libvorbis, libGLU, SDL2, freetype, alsaLib
+}:
+
+buildGame rec {
+  name = "trine2-${version}";
+  version = "2.01";
+
+  src = fetchHumbleBundle {
+    machineName = "trine2complete_linux";
+    suffix = "zip";
+    md5 = "82049b65c1bce6841335935bc05139c8";
+  };
+
+  nativeBuildInputs = [ makeWrapper ];
+  buildInputs = [ openal libvorbis libGLU freetype alsaLib ];
+
+  patchPhase = ''
+    patchelf --replace-needed libSDL-1.3.so.0 libSDL.so \
+      bin/trine2_linux_32bit
+    patchelf --replace-needed libPhysXLoader.so.1 libPhysXLoader.so \
+      bin/trine2_linux_32bit
+  '';
+
+  getResolutionArgs = runCommandCC "get-resolution-args" {
+    buildInputs = [ SDL2 ];
+    src = writeText "get-resolution-args.c" ''
+      #include <SDL.h>
+
+      int main(void)
+      {
+        int width = 0, height = 0;
+        SDL_DisplayMode current;
+
+        SDL_Init(SDL_INIT_VIDEO);
+
+        int displays = SDL_GetNumVideoDisplays();
+
+        for (int i = 0; i < displays; ++i) {
+          if (SDL_GetCurrentDisplayMode(i, &current) != 0)
+            goto err;
+
+          if (current.w * current.h > width * height) {
+            width = current.w;
+            height = current.h;
+          }
+        }
+
+        if (width == 0 && height == 0)
+          goto err;
+
+        SDL_Quit();
+        printf("-RenderingModule:DetectedFullscreenWidth=%d\n", width);
+        printf("-RenderingModule:DetectedFullscreenHeight=%d\n", height);
+        return EXIT_SUCCESS;
+
+      err:
+        fputs("Unable to get current display mode.\n", stderr);
+        SDL_Quit();
+        return EXIT_FAILURE;
+      }
+    '';
+  } "gcc -Wall $(sdl2-config --cflags --libs) -o \"$out\" \"$src\"";
+
+  installPhase = ''
+    for name in Cg CgGL PhysXCooking PhysXCore PhysXLoader; do
+      install -vD "lib/lib32/lib$name.so" "$out/libexec/trine2/lib$name.so"
+    done
+
+    install -vD lib/lib32/libSDL-1.3.so.0 "$out/libexec/trine2/libSDL.so"
+
+    mkdir -p "$out/share/trine2"
+    cp -rvt "$out/share/trine2" *.fbq trine2.png data
+
+    install -vD bin/trine2_linux_32bit "$out/libexec/trine2/trine2"
+
+    rtDataPath="\''${XDG_DATA_HOME:-\$HOME/.local/share}"
+    makeWrapper "$out/libexec/trine2/trine2" "$out/bin/trine2" \
+      --run "cd '$out/share/trine2'" \
+      --run '${coreutils}/bin/ln -s "'"$rtDataPath"'" "$HOME/.frozenbyte"' \
+      --prefix LD_LIBRARY_PATH : "$out/libexec/trine2" \
+      --add-flags "\$($getResolutionArgs)"
+
+    mkdir -p "$out/share/applications"
+    cat > "$out/share/applications/trine2.desktop" <<EOF
+    [Desktop Entry]
+    Name=Trine 2
+    Type=Application
+    Version=1.1
+    Exec=$out/bin/trine2
+    Icon=$out/share/trine2/trine2.png
+    Categories=Game
+    EOF
+  '';
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/Trine2" ];
+}
diff --git a/pkgs/games/humblebundle/unepic.nix b/pkgs/games/humblebundle/unepic.nix
new file mode 100644
index 00000000..cc4099f5
--- /dev/null
+++ b/pkgs/games/humblebundle/unepic.nix
@@ -0,0 +1,44 @@
+{ stdenv, fetchHumbleBundle, unzip, makeWrapper, SDL2, SDL2_mixer, zlib }:
+
+let
+  version = "1.50.5";
+  versionName = "15005";
+  arch = { 
+    "i686-linux" = "32";
+    "x86_64-linux" = "64";
+  }.${stdenv.system};
+in stdenv.mkDerivation rec {
+  name = "unepic-${version}";
+
+  src = fetchHumbleBundle {
+    name = "unepic-15005.run";
+    machineName = "unepic_linux";
+    downloadName = ".run";
+    md5 = "940824c4de6e48522845f63423e87783";
+  };
+
+  phases = [ "installPhase" ];
+
+  buildInputs = [ unzip makeWrapper ];
+
+  installPhase = let
+    rpath = stdenv.lib.makeLibraryPath [ SDL2 SDL2_mixer zlib stdenv.cc.cc ];
+  in ''
+    dest="$out/opt/games/unepic"
+    exe="$dest/unepic${arch}"
+
+    mkdir -p "$out/opt/games"
+    unzip "$src" "data/*" -d "$out/opt/games" || [ "$?" -eq 1 ]
+    mv "$out/opt/games/data" "$dest"
+    rm -r "$dest"/lib*
+
+    # Patch $exe acccording to arch.
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${rpath}" "$exe"
+
+    mkdir -p "$out/bin"
+
+    makeWrapper "$exe" "$out/bin/unepic" --run "cd '$dest'"
+  '';
+}
diff --git a/pkgs/games/itch/default.nix b/pkgs/games/itch/default.nix
new file mode 100644
index 00000000..cae4b99f
--- /dev/null
+++ b/pkgs/games/itch/default.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.itch;
+
+  self = rec {
+    callPackage = pkgs.lib.callPackageWith (pkgs // self);
+    callPackage_i686 = pkgs.lib.callPackageWith (pkgs.pkgsi686Linux // self);
+
+    fetchItch = callPackage ./fetch-itch {
+      inherit (config.itch) apiKey;
+    };
+
+    invisigun-heroes = callPackage ./invisigun-heroes.nix {};
+    towerfall-ascension = callPackage ./towerfall-ascension.nix {};
+  };
+in {
+  options.itch.apiKey = lib.mkOption {
+    type = lib.types.nullOr lib.types.str;
+    default = null;
+    description = ''
+      The API key of your <link xlink:href="https://itch.io/">itch.io</link>
+      account, can be retrieved by heading to <link
+      xlink:href="https://itch.io/user/settings/api-keys"/>.
+    '';
+  };
+
+  config.packages.itch = lib.mkIf (cfg.apiKey != null) self;
+}
diff --git a/pkgs/games/itch/fetch-itch/default.nix b/pkgs/games/itch/fetch-itch/default.nix
new file mode 100644
index 00000000..121868be
--- /dev/null
+++ b/pkgs/games/itch/fetch-itch/default.nix
@@ -0,0 +1,81 @@
+{ stdenv, curl, cacert, writeText, python3Packages
+
+, apiKey
+}:
+
+{ name, gameId, uploadId, sha256, version ? null }:
+
+let
+  cafile = "${cacert}/etc/ssl/certs/ca-bundle.crt";
+
+  getDownloadURL = writeText "getitch.py" ''
+    import os, sys, json
+
+    from urllib.parse import urljoin
+    from urllib.request import urlopen
+
+    API_KEY = os.getenv('apiKey')
+    API_URL = 'https://itch.io/api/1/'
+    API_BASE = urljoin(API_URL, API_KEY) + '/'
+
+    NAME = os.getenv('name')
+    GAME_ID = int(os.getenv('gameId'))
+    UPLOAD_ID = int(os.getenv('uploadId'))
+    VERSION = os.getenv('version', None)
+
+    def request(path):
+      with urlopen(urljoin(API_BASE, path)) as u:
+        return json.loads(u.read())
+
+    def get_versions(key):
+      url = 'upload/{}/builds'.format(UPLOAD_ID)
+      return request(url + '?download_key_id=' + str(key))['builds']
+
+    def print_download_url(key):
+      if VERSION is not None:
+        versions = get_versions(key)
+        wanted = [ver for ver in versions if ver['user_version'] == VERSION]
+        if len(wanted) == 1:
+          url = 'upload/{}/download/builds/{}?download_key_id={}'.format(
+            UPLOAD_ID, wanted[0]['id'], key
+          )
+          sys.stdout.write(request(url)['archive']['url'] + '\n')
+          return
+        else:
+          msg = 'Unknown version {}, recent versions are:\n'.format(VERSION)
+          sys.stderr.write(msg)
+          for ver in versions[:20]:
+            sys.stderr.write('Update date: {}, version: {}\n'.format(
+              ver['updated_at'], ver['user_version']
+            ))
+          raise SystemExit(1)
+
+      url = 'download-key/{}/download/{}'.format(key, UPLOAD_ID)
+      sys.stdout.write(request(url)['url'] + '\n')
+
+    for key in request('my-owned-keys')['owned_keys']:
+      if key['game']['id'] == GAME_ID:
+        print_download_url(key['id'])
+        raise SystemExit(0)
+
+    sys.stderr.write('Unable to find download for game {}!'.format(NAME))
+    raise SystemExit(1)
+  '';
+
+in stdenv.mkDerivation {
+  inherit name apiKey gameId uploadId version;
+  outputHashAlgo = "sha256";
+  outputHash = sha256;
+
+  SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
+
+  preferLocalBuild = true;
+  nativeBuildInputs = [ python3Packages.python ];
+
+  buildCommand = ''
+    url="$(python "${getDownloadURL}")"
+    header "downloading $name from $url"
+    "${curl.bin or curl}/bin/curl" --fail --output "$out" "$url"
+    stopNest
+  '';
+}
diff --git a/pkgs/games/itch/invisigun-heroes.nix b/pkgs/games/itch/invisigun-heroes.nix
new file mode 100644
index 00000000..15aba338
--- /dev/null
+++ b/pkgs/games/itch/invisigun-heroes.nix
@@ -0,0 +1,43 @@
+{ buildUnity, fetchItch, mono, monogamePatcher, strace }:
+
+buildUnity rec {
+  name = "invisigun-heroes";
+  fullName = "Invisigun";
+  saveDir = "Sombr Studio/Invisigun Reloaded";
+  version = "1.7.16";
+
+  src = fetchItch {
+    name = "${name}-${version}.zip";
+    gameId = 25561;
+    uploadId = 208583;
+    version = "v${version}";
+    sha256 = "1flwc5wvw84s53my8v8n402iz6izjs4d4ppffajdv9cg1vs3nbpl";
+  };
+
+  nativeBuildInputs = [ mono monogamePatcher ];
+
+  buildPhase = ''
+    cat > nix-support.cs <<EOF
+    using UnityEngine;
+
+    public class NixSupport {
+      public static string GetFullPathStub(string _ignore) {
+        return Application.persistentDataPath;
+      }
+    }
+    EOF
+
+    mcs nix-support.cs -target:library \
+      -r:Invisigun_Data/Managed/UnityEngine.CoreModule \
+      -out:Invisigun_Data/Managed/NixSupport.dll
+
+    monogame-patcher replace-call \
+      -i Invisigun_Data/Managed/Assembly-CSharp.dll \
+      -a Invisigun_Data/Managed/NixSupport.dll \
+      'System.String System.IO.Path::GetFullPath(System.String)' \
+      'System.String NixSupport::GetFullPathStub(System.String)' \
+      FileManagerAdapter_Desktop::ApplicationPath
+  '';
+
+  sandbox.paths.required = [ "$HOME/Invisigun Heroes" ];
+}
diff --git a/pkgs/games/itch/towerfall-ascension.nix b/pkgs/games/itch/towerfall-ascension.nix
new file mode 100644
index 00000000..5028bc98
--- /dev/null
+++ b/pkgs/games/itch/towerfall-ascension.nix
@@ -0,0 +1,90 @@
+{ stdenv, lib, buildGame, fetchItch, makeWrapper, p7zip, unzip, mono
+, SDL2, SDL2_image, libGL, libvorbis, openal, monogamePatcher, writeScriptBin
+, coreutils
+
+, darkWorldExpansion ? true
+}:
+
+buildGame rec {
+  name = "towerfall-ascension-${version}";
+  version = "20160723";
+
+  srcs = lib.singleton (fetchItch {
+    name = "${name}.bin";
+    gameId = 22755;
+    uploadId = 243755;
+    sha256 = "01ipq3z0c2k4h88r7j17nfp43p5sav12a9syangqm0syflvwqxb6";
+  }) ++ lib.optional darkWorldExpansion (fetchItch {
+    name = "towerfall-darkworld.zip";
+    gameId = 24962;
+    uploadId = 216070;
+    sha256 = "1nb26m2l74rsnlwv9mv33l2s5n873867k9zypc84sm3iljvrdkmg";
+  });
+
+  unpackCmd = ''
+    case "$curSrc" in
+      *.bin) ${p7zip}/bin/7z x "$curSrc" data;;
+      *.zip) ${unzip}/bin/unzip -qq "$curSrc" -d data;;
+      *) false;;
+    esac
+  '';
+
+  patchPhase = ''
+    monogame-patcher fix-filestreams -i TowerFall.exe \
+      Texture IntroScene SFX SFXVaried
+  '';
+
+  nativeBuildInputs = [ makeWrapper mono monogamePatcher ];
+
+  libdir = if stdenv.system == "x86_64-linux" then "lib64" else "lib";
+
+  buildPhase = let
+    dllmap = {
+      SDL2 = "${SDL2}/lib/libSDL2.so";
+      SDL2_image = "${SDL2_image}/lib/libSDL2_image.so";
+      soft_oal = "${openal}/lib/libopenal.so";
+      libvorbisfile-3 = "${libvorbis}/lib/libvorbisfile.so";
+      MojoShader = "$out/libexec/towerfall-ascension/libmojoshader.so";
+    };
+  in lib.concatStrings (lib.mapAttrsToList (dll: target: ''
+    sed -i -e '/<dllmap.*dll="${dll}\.dll".*os="linux"/ {
+      s!target="[^"]*"!target="'"${target}"'"!
+    }' FNA.dll.config
+  '') dllmap);
+
+  dummyXdgOpen = writeScriptBin "xdg-open" ''
+    #!${stdenv.shell} -e
+    if [ "''${1##*.}" = txt ]; then
+      exec ${coreutils}/bin/head -v -n 20 "$1"
+    else
+      echo "Unable to open file $1" >&2
+      exit 1
+    fi
+  '';
+
+  installPhase = ''
+    mkdir -p "$out/bin" \
+             "$out/share/towerfall-ascension" \
+             "$out/libexec/towerfall-ascension"
+    cp -rvt "$out/share/towerfall-ascension" Content
+    cp -rv mono/config "$out/libexec/towerfall-ascension/TowerFall.exe.config"
+    cp -rvt "$out/libexec/towerfall-ascension" TowerFall.exe FNA.dll* \
+      "$libdir/libmojoshader.so"
+    ln -s "$out/share/towerfall-ascension/Content" \
+          "$out/libexec/towerfall-ascension/Content"
+
+    if [ -e "TowerFall Dark World Expansion" ]; then
+      cp -rvt "$out/share/towerfall-ascension" \
+        "TowerFall Dark World Expansion/DarkWorldContent"
+    fi
+
+    makeWrapper ${lib.escapeShellArg mono}/bin/mono \
+      "$out/bin/towerfall-ascension" \
+      --set SDL_OPENGL_LIBRARY ${lib.escapeShellArg "${libGL}/lib/libGL.so"} \
+      --set PATH "$dummyXdgOpen/bin" \
+      --add-flags "$out/libexec/towerfall-ascension/TowerFall.exe" \
+      --run "cd '$out/share/towerfall-ascension'"
+  '';
+
+  sandbox.paths.required = [ "$XDG_DATA_HOME/TowerFall" ];
+}
diff --git a/pkgs/games/steam/default.nix b/pkgs/games/steam/default.nix
new file mode 100644
index 00000000..03e6b180
--- /dev/null
+++ b/pkgs/games/steam/default.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.steam;
+
+  self = rec {
+    callPackage = pkgs.lib.callPackageWith (pkgs // self);
+
+    fetchSteam = callPackage ./fetchsteam {
+      inherit (config.steam) username password;
+    };
+
+    starbound = callPackage ./starbound.nix { flavor = "stable"; };
+    starbound-unstable = callPackage ./starbound.nix { flavor = "unstable"; };
+  };
+in with lib; {
+  options.steam = {
+    username = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        User name for your Steam account.
+      '';
+    };
+
+    password = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Password for your Steam account.
+      '';
+    };
+  };
+
+  config.packages = {
+    steam = mkIf (cfg.username != null && cfg.password != null) self;
+  };
+}
diff --git a/pkgs/games/steam/fetchsteam/default.nix b/pkgs/games/steam/fetchsteam/default.nix
new file mode 100644
index 00000000..646e0a14
--- /dev/null
+++ b/pkgs/games/steam/fetchsteam/default.nix
@@ -0,0 +1,93 @@
+{ stdenv, runCommand, writeText, fetchFromGitHub, buildDotnetPackage
+, username, password
+}:
+
+{ name, appId, depotId, manifestId, branch ? null, sha256, fileList ? [] }:
+
+let
+  protobuf-net = buildDotnetPackage rec {
+    baseName = "protobuf-net";
+    version = "2.0.0.668";
+
+    src = fetchFromGitHub {
+      owner = "mgravell";
+      repo = "protobuf-net";
+      rev = "r668";
+      sha256 = "1060pihqkbr9pd2z6m01d6fsbc9nj56m6y5a0pch9mqdmviv4896";
+    };
+
+    sourceRoot = "${src.name}/${baseName}";
+  };
+
+  SteamKit2 = buildDotnetPackage rec {
+    baseName = "SteamKit2";
+    version = "1.6.4";
+
+    src = fetchFromGitHub {
+      owner = "SteamRE";
+      repo = "SteamKit";
+      rev = "SteamKit_${version}";
+      sha256 = "17d7wi2f396qhp4w9sf37lazvsaqws8x071hfis9gv5llv6s7q46";
+    };
+
+    buildInputs = [ protobuf-net ];
+
+    xBuildFiles = [ "SteamKit2/SteamKit2.sln" ];
+    outputFiles = [ "SteamKit2/SteamKit2/bin/Release/*" ];
+  };
+
+  DepotDownloader = buildDotnetPackage rec {
+    baseName = "DepotDownloader";
+    version = "2.1.1git20160207";
+
+    src = fetchFromGitHub {
+      owner = "SteamRE";
+      repo = baseName;
+      rev = "5fa6621d9f9448fcd20c974b427a8bd2cb044cb4";
+      sha256 = "0vb566d7x1scd96c8ybq6gdbc2cv5jjq453ld458qcvfy587amfn";
+    };
+
+    patches = [ ./downloader.patch ];
+
+    postPatch = ''
+      sed -i \
+        -e 's/\(<[Rr]eference *[Ii]nclude="[^", ]\+\)[^"]*/\1/g' \
+        -e 's,<[Ss]pecific[Vv]ersion>[Tt]rue</[Ss]pecific[Vv]ersion>,,g' \
+        DepotDownloader/DepotDownloader.csproj
+      sed -i -e 's/ version="[^"]*"//g' DepotDownloader/packages.config
+    '';
+
+    buildInputs = [ SteamKit2 protobuf-net ];
+
+    outputFiles = [ "${baseName}/bin/Release/*" ];
+
+    # UUUGLY, but I don't want to spend a week trying to get this working
+    # without that nasty wrapper.
+    makeWrapperArgs = let
+      mkMono = name: path: "${path}/lib/dotnet/${name}";
+      paths = stdenv.lib.mapAttrsToList mkMono {
+        inherit SteamKit2 protobuf-net;
+      };
+      monoPath = stdenv.lib.concatStringsSep ":" paths;
+    in [ "--prefix MONO_PATH : \"${monoPath}\"" ];
+  };
+
+  fileListFile = let
+    content = stdenv.lib.concatStringsSep "\n" fileList;
+  in writeText "steam-file-list-${name}.txt" content;
+
+in with stdenv.lib; runCommand "${name}-src" {
+  buildInputs = [ DepotDownloader ];
+  inherit username password appId depotId manifestId;
+  preferLocalBuild = true;
+  outputHashAlgo = "sha256";
+  outputHash = sha256;
+  outputHashMode = "recursive";
+} ''
+  depotdownloader -app "$appId" -depot "$depotId" -manifest "$manifestId" \
+    ${optionalString (fileList != []) "-filelist \"${fileListFile}\""} \
+    ${optionalString (branch != null) "-branch \"${branch}\""} \
+    -username "$username" -password "$password" -dir "$out"
+  rm -r "$out/.DepotDownloader"
+  rm "$out/_steam_depot_manifest_$depotId.csv"
+''
diff --git a/pkgs/games/steam/fetchsteam/downloader.patch b/pkgs/games/steam/fetchsteam/downloader.patch
new file mode 100644
index 00000000..72e5c473
--- /dev/null
+++ b/pkgs/games/steam/fetchsteam/downloader.patch
@@ -0,0 +1,34 @@
+diff --git a/DepotDownloader/ContentDownloader.cs b/DepotDownloader/ContentDownloader.cs
+index 21c317e..81f2a93 100644
+--- a/DepotDownloader/ContentDownloader.cs
++++ b/DepotDownloader/ContentDownloader.cs
+@@ -34,7 +34,7 @@ namespace DepotDownloader
+             public string installDir { get; private set; }
+             public string contentName { get; private set; }
+ 
+-            public ulong manifestId { get; private set; }
++            public ulong manifestId { get; set; }
+             public byte[] depotKey;
+ 
+             public DepotDownloadInfo(uint depotid, ulong manifestId, string installDir, string contentName)
+@@ -198,9 +198,6 @@ namespace DepotDownloader
+ 
+         static ulong GetSteam3DepotManifest(uint depotId, uint appId, string branch)
+         {
+-            if (Config.ManifestId != INVALID_MANIFEST_ID)
+-                return Config.ManifestId;
+-
+             KeyValue depots = GetSteam3AppSection(appId, EAppInfoSection.Depots);
+             KeyValue depotChild = depots[depotId.ToString()];
+ 
+@@ -583,6 +580,10 @@ namespace DepotDownloader
+                 ConfigStore.TheConfig.LastManifests[depot.id] = INVALID_MANIFEST_ID;
+                 ConfigStore.Save();
+ 
++                Console.WriteLine("Latest manifest ID is {0}.", depot.manifestId);
++                if (Config.ManifestId != INVALID_MANIFEST_ID)
++                    depot.manifestId = Config.ManifestId;
++
+                 if (lastManifestId != INVALID_MANIFEST_ID)
+                 {
+                     var oldManifestFileName = Path.Combine(configDir, string.Format("{0}.bin", lastManifestId));
diff --git a/pkgs/games/steam/starbound.nix b/pkgs/games/steam/starbound.nix
new file mode 100644
index 00000000..b18e2e33
--- /dev/null
+++ b/pkgs/games/steam/starbound.nix
@@ -0,0 +1,207 @@
+{ stdenv, fetchSteam, fetchurl, writeText, SDL, libGL, jq, makeDesktopItem
+, flavor ? "stable"
+}:
+
+let
+  renameAttrs = f: let
+    rename = name: value: {
+      name = f name;
+      inherit value;
+    };
+  in stdenv.lib.mapAttrs' rename;
+
+  darwinize = renameAttrs (bin: "Starbound.app/Contents/MacOS/${bin}");
+  winize = renameAttrs (bin: "${bin}.exe");
+
+  mkOsBinaryDeps = with stdenv.lib;
+    if stdenv.system == "x86_64-darwin" then darwinize
+    else if elem stdenv.system [ "i686-cygwin" "x86_64-cygwin" ] then winize
+    else id;
+
+  binaryDeps = mkOsBinaryDeps {
+    starbound.deps = [ SDL libGL ];
+    starbound.hasBootconfigArg = true;
+    starbound_server.name = "starbound-server";
+    starbound_server.hasBootconfigArg = true;
+    asset_packer.name = "starbound-asset-packer";
+    asset_unpacker.name = "starbound-asset-unpacker";
+    dump_versioned_json.name = "starbound-dump-versioned-json";
+    make_versioned_json.name = "starbound-make-versioned-json";
+    planet_mapgen.name = "starbound-planet-mapgen";
+  };
+
+  binpath = if stdenv.system == "x86_64-linux" then "linux64"
+            else if stdenv.system == "i686-linux" then "linux32"
+            else if stdenv.system == "x86_64-darwin" then "osx"
+            else if stdenv.system == "i686-cygwin" then "win32"
+            else if stdenv.system == "x86_64-cygwin" then "win64"
+            else throw "Unsupported system ${stdenv.system} for Starbound";
+
+  throwUnsupported = throw "Unsupported flavor `${flavor}', use either "
+                         + "`stable' or `unstable'.";
+
+  upstreamInfo = if flavor == "stable" then {
+    name = "starbound";
+    version = "20151216";
+    appId = 211820;
+    depotId = 211821;
+    manifestId = 1842730272313189605;
+    sha256 = "0qppfn56c778wsg38hi6sxgi3rl9nv72h9rmmxybi1vzpf3p49py";
+  } else if flavor == "unstable" then {
+    name = "starbound-unstable";
+    version = "20160223";
+    appId = 367540;
+    depotId = 367541;
+    manifestId = 6970641909803280413;
+    sha256 = "0qppfn56c778wsg38hi6sxgi3rl9nv72h9rmmxybi1vzpf3p49py";
+  } else throwUnsupported;
+
+  upstream = fetchSteam {
+    inherit (upstreamInfo) name appId depotId manifestId sha256;
+    fileList = [
+      "^(?:assets|tiled)/"
+      ( "^${binpath}(?:/Starbound\\.app/Contents/MacOS)?"
+      + "/(?:[a-zA-Z0-9_-]+(?:\\.exe)?|sbboot\\.config)$")
+    ];
+  };
+
+  staticBootOverrides = writeText "bootconfig.overrides" (builtins.toJSON {
+    assetSources = [
+      "${upstream}/assets/packed.pak"
+      "${upstream}/assets/user"
+    ];
+  });
+
+  bootOverrides = {
+    storageDirectory = "$XDG_DATA_HOME/${settingsDir}/";
+    modSource = "$XDG_DATA_HOME/${settingsDir}/mods/";
+  };
+
+  settingsDir =
+    if flavor == "stable" then "starbound"
+    else if flavor == "unstable" then "starbound-unstable"
+    else throwUnsupported;
+
+  mkProg = bin: attrs: let
+    basename = builtins.baseNameOf bin;
+
+    hasBootconfigArg = attrs.hasBootconfigArg or false;
+
+    bootconfigArgs = with stdenv.lib; let
+      mkArg = opt: val: "-${opt} \"${val}\"";
+    in " " + (concatStringsSep " " (mapAttrsToList mkArg {
+      bootconfig = "$XDG_DATA_HOME/${settingsDir}/sbboot.config";
+    }));
+
+    wrapper = writeText "starbound-wrapper.sh" ''
+      #!${stdenv.shell} -e
+      [ -n "$XDG_DATA_HOME" ] || XDG_DATA_HOME="$HOME/.local/share"
+
+      mkdir -p "${bootOverrides.storageDirectory}" \
+               "${bootOverrides.modSource}"
+
+      "${jq}/bin/jq" -s '.[0] * .[1]' "@out@/etc/sbboot.config" - \
+        > "$XDG_DATA_HOME/${settingsDir}/sbboot.config" \
+      <<BOOTCONFIG_OVERRIDES
+      ${builtins.toJSON bootOverrides}
+      BOOTCONFIG_OVERRIDES
+
+      ${if hasBootconfigArg then ''
+        # This is needed because Starbound aborts if
+        # a command line argument is specified twice.
+        hasBootconfigArg() {
+          while [ $# -gt 0 ]; do
+            if [ "x''${1#-}" != "x$1" ]; then
+              case "''${1#-}" in
+                bootconfig) return 0;;
+                # Arguments that expect a parameter
+                loglevel|logfile|configfile|setconfig) shift;;
+              esac
+            fi
+            shift
+          done
+          return 1
+        }
+
+        if hasBootconfigArg "$@"; then
+          exec "@out@/libexec/starbound/${basename}" "$@"
+        else
+          exec "@out@/libexec/starbound/${basename}"${bootconfigArgs} "$@"
+        fi
+      '' else ''
+        exec "@out@/libexec/starbound/${basename}" "$@"
+      ''}
+    '';
+
+  in ''
+    install -vD "patched/${basename}" "$out/libexec/starbound/${basename}"
+    substituteAll "${wrapper}" "$out/bin/${attrs.name or basename}"
+    chmod +x "$out/bin/${attrs.name or basename}"
+  '';
+
+  desktopItem = makeDesktopItem {
+    name = "starbound";
+    exec = "starbound";
+    icon = fetchurl {
+      url = "http://i1305.photobucket.com/albums/s544/ClockworkBarber/"
+          + "logo_zps64c4860d.png";
+      sha256 = "11fiiy0vcxzix1j81732cjh16wi48k4vag040vmbhad50ps3mg0q";
+    };
+    comment = "An extraterrestrial sandbox adventure game";
+    desktopName = "Starbound";
+    genericName = "starbound";
+    categories = "Game;";
+  };
+
+  patchBinary = bin: attrs: ''
+    mkdir -p "patched/$(dirname "${bin}")"
+    cp -t "patched/$(dirname "${bin}")" "$upstream/$binpath/${bin}"
+    chmod +x "patched/$(basename "${bin}")"
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${stdenv.lib.makeLibraryPath (attrs.deps or [])}" \
+      "patched/$(basename "${bin}")"
+    if ldd "patched/$(basename "${bin}")" | grep -F 'not found'; then
+      exit 1;
+    fi
+  '';
+
+in stdenv.mkDerivation {
+  name = "${upstreamInfo.name}-${upstreamInfo.version}";
+  inherit (upstreamInfo) version;
+
+  unpackPhase = ":";
+
+  inherit binpath upstream;
+
+  buildPhase = with stdenv.lib;
+    concatStrings (mapAttrsToList patchBinary binaryDeps);
+
+  doCheck = true;
+
+  checkPhase = ''
+    checkFailed=
+    for i in "$upstream/$binpath"/*; do
+      [ -f "$i" ] || continue
+      [ "$(basename "$i")" != sbboot.config ] || continue
+      [ "$(basename "$i")" != launcher ] || continue
+      [ ! -e "patched/$(basename "$i")" ] || continue
+
+      echo "Found missing binary $i from the upstream tree."
+      checkFailed=1
+    done
+    [ -z "$checkFailed" ]
+  '';
+
+  installPhase = ''
+    mkdir -p "$out/bin" "$out/etc"
+    sed -e 's,//.*,,' "${upstream}/${binpath}/sbboot.config" \
+      | "${jq}/bin/jq" -s '.[0] * .[1]' - "${staticBootOverrides}" \
+      > "$out/etc/sbboot.config"
+    ${stdenv.lib.concatStrings (stdenv.lib.mapAttrsToList mkProg binaryDeps)}
+    install -m 0644 -vD "${desktopItem}/share/applications/starbound.desktop" \
+      "$out/share/applications/starbound.desktop"
+  '';
+
+  dontStrip = true;
+}
diff --git a/pkgs/lib/call-package-scope.nix b/pkgs/lib/call-package-scope.nix
new file mode 100644
index 00000000..75c19faf
--- /dev/null
+++ b/pkgs/lib/call-package-scope.nix
@@ -0,0 +1,25 @@
+{ pkgs, pkgsi686Linux }:
+
+fn: let
+  inherit (builtins) isFunction intersectAttrs functionArgs;
+
+  f = if isFunction fn then fn else import fn;
+
+  autoArgs = pkgs // {
+    callPackage = pkgs.lib.callPackageWith (pkgs // super);
+    callPackage_i686 = pkgs.lib.callPackageWith (pkgsi686Linux // super);
+  };
+  args = intersectAttrs (functionArgs f) autoArgs;
+
+  mkOverridable = overrideFun: origArgs: let
+    superSet = overrideFun origArgs;
+    overrideWith = newArgs: let
+      overridden = if isFunction newArgs then newArgs origArgs else newArgs;
+    in origArgs // overridden;
+  in superSet // {
+    override = newArgs: mkOverridable overrideFun (overrideWith newArgs);
+  };
+
+  super = mkOverridable f args;
+
+in pkgs.recurseIntoAttrs super
diff --git a/pkgs/list-gamecontrollers/default.nix b/pkgs/list-gamecontrollers/default.nix
new file mode 100644
index 00000000..15a9e03c
--- /dev/null
+++ b/pkgs/list-gamecontrollers/default.nix
@@ -0,0 +1,10 @@
+{ runCommandCC, pkgconfig, SDL2 }:
+
+runCommandCC "list-gamecontrollers" {
+  buildInputs = [ pkgconfig SDL2 ];
+} ''
+  mkdir -p "$out/bin"
+  cc -Werror "${./list-gc.c}" \
+    $(pkg-config --libs --cflags sdl2) \
+    -o "$out/bin/list-gamecontrollers"
+''
diff --git a/pkgs/list-gamecontrollers/list-gc.c b/pkgs/list-gamecontrollers/list-gc.c
new file mode 100644
index 00000000..f40b7da6
--- /dev/null
+++ b/pkgs/list-gamecontrollers/list-gc.c
@@ -0,0 +1,31 @@
+#include <SDL.h>
+
+void dump_guid(SDL_Joystick *js) {
+    SDL_JoystickGUID guid;
+    const char *name;
+    char guidstr[33];
+
+    guid = SDL_JoystickGetGUID(js);
+    name = SDL_JoystickName(js);
+    SDL_JoystickGetGUIDString(guid, guidstr, sizeof(guidstr));
+
+    printf("%s: %s\n", name, guidstr);
+}
+
+int main()
+{
+    int i;
+    SDL_Joystick *js;
+
+    SDL_Init(SDL_INIT_JOYSTICK);
+    atexit(SDL_Quit);
+
+    for (i = 0; i < SDL_NumJoysticks(); ++i) {
+        if ((js = SDL_JoystickOpen(i)) != NULL) {
+            dump_guid(js);
+            SDL_JoystickClose(js);
+        }
+    }
+
+    return EXIT_SUCCESS;
+}
diff --git a/pkgs/openlab/default.nix b/pkgs/openlab/default.nix
new file mode 100644
index 00000000..131a3eca
--- /dev/null
+++ b/pkgs/openlab/default.nix
@@ -0,0 +1,7 @@
+{ callPackage, haskell }:
+
+{
+  gitit = callPackage ./gitit { hlib = haskell.lib; };
+  # TODO: fix haskell code
+  # stackenblocken = callPackage ./stackenblocken {};
+}
diff --git a/pkgs/openlab/gitit/default.nix b/pkgs/openlab/gitit/default.nix
new file mode 100644
index 00000000..dca2822a
--- /dev/null
+++ b/pkgs/openlab/gitit/default.nix
@@ -0,0 +1,18 @@
+{ hlib, haskellPackages, fetchFromGitHub }:
+
+let hp = haskellPackages.override {
+  overrides = (self: super: {
+    gitit = (hlib.overrideCabal super.gitit (drv: rec {
+      src = fetchFromGitHub {
+        owner = "openlab-aux";
+        repo = "gitit";
+        rev = "3da7c841f9382d0c62242a1b718511acec97e9f7";
+        sha256 = "0qhkbvm4ixav4nln3m9845w9m3gzfq5xh4nxp2c9qj4w9p79if7z";
+      };
+      broken = true;
+      platforms = [ "x86_64-linux" ];
+      hydraPlatforms = platforms;
+    }));
+  });
+};
+in hp.gitit
diff --git a/pkgs/openlab/stackenblocken/default.nix b/pkgs/openlab/stackenblocken/default.nix
new file mode 100644
index 00000000..0df21a31
--- /dev/null
+++ b/pkgs/openlab/stackenblocken/default.nix
@@ -0,0 +1,86 @@
+{ lib, fetchFromGitHub, writeScriptBin, curl, bash, gawk
+, haskellPackages, mpg321
+, volumePercent ? 50 }:
+
+let
+  repo = fetchFromGitHub {
+    owner = "openlab-aux";
+    repo = "stackenblocken";
+    rev = "2fee7083f243a33d800f13afa2edc432220e3c77";
+    sha256 = "1qri0m16pxbq4rzp38l3my2yiisl2699zmq5vdz7cqfi4vpbwl91";
+ };
+
+ bot = haskellPackages.callPackage "${repo}/stackenblocken.nix" {};
+ jingle = "${repo}/stackenblocken_jingle.mp3";
+
+ script = ''
+    #!${lib.getBin bash}/bin/bash
+    percent=10
+    no_stackenblocken="no STACKENBLOCKEN today"
+    tmpd=$(mktemp -d)
+
+    # kill everything on SIGINT
+    trap exit SIGINT
+    # also running background processes
+    trap "kill 0" EXIT
+
+    function icsfile {
+      ${lib.getBin gawk}/bin/awk -v date=''${1:-nodate} '
+        /BEGIN:VEVENT/ { cache = 1; }
+        /DTSTART:/ {
+          if( index( $0, date ) )
+            printf( "%s", cached_lines );
+          else
+            drop = 1;
+          cached_lines = "";
+          cache = 0;
+        }
+        cache  {
+          cached_lines = cached_lines $0 "\n";
+          next;
+        };
+        !drop { print; }
+        /END:VEVENT/ { drop = 0; }
+      '
+    }
+
+    function check_events {
+      ${lib.getBin curl}/bin/curl -s https://openlab-augsburg.de/veranstaltungen/events.ics \
+        | icsfile `date --utc +%Y%m%d` \
+        > "$tmpd/events-today"
+
+      # filter out events that have the no-stackenblocken tag
+      # and skip it on those days
+      if <"$tmpd/events-today" grep -q "CATEGORIES.*no-stackenblocken"; then
+        events=$(<$tmpd/events-today sed -ne 's/SUMMARY:\(.*\)$/\1/p')
+        echo "$no_stackenblocken because of event(s):"
+        echo "$events"
+        exit 0
+      fi
+    }
+
+    function check_random {
+      rnumber=$RANDOM
+      ((rnumber %= 100))
+      # lt for percent (numbers begin from 0)
+      if [ $rnumber -lt $percent ]; then
+        echo "$no_stackenblocken because lucks says so! ($percent% chance)"
+        exit 0
+      fi
+    }
+
+    check_events
+    check_random
+
+    for i in $(seq 2); do
+      echo "starting .labping bot"
+      ${lib.getBin bot}/bin/stackenblocken &
+      echo "DOING STACKENBLOCKEN"
+      ${lib.getBin mpg321}/bin/mpg321 --gain ${toString volumePercent} -q ${jingle}
+    done
+  '';
+
+
+in
+  writeScriptBin "stackenblocken" script
+
diff --git a/pkgs/profpatsch/backlight/backlight.py b/pkgs/profpatsch/backlight/backlight.py
new file mode 100755
index 00000000..d580260a
--- /dev/null
+++ b/pkgs/profpatsch/backlight/backlight.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+# xbacklight uses a linear percentage,
+# but the backlight intensity is actually logarithmic *facepalm*.
+# So we read the current "percentage" given by xbacklight
+# and calculate the next step, base 2.
+
+import subprocess as sub
+import math
+import sys
+
+xbacklight = "xbacklight"
+
+def usage():
+    print("usage: backlight [inc|dec]", file=sys.stderr)
+    sys.exit(1)
+
+# read current value
+current_backlight = float(sub.run(xbacklight, stdout=sub.PIPE).stdout.strip())
+# find the actual value, base 2
+current_val = round(math.sqrt(current_backlight))
+
+if len(sys.argv) == 1: usage()
+else:
+    mode = sys.argv[1]
+
+# modify actual value
+if mode == "inc":
+    new_val = current_val + 1
+elif mode == "dec":
+    new_val = current_val - 1
+else:
+    usage()
+
+# clamp value
+new_backlight = min(10, max(0, new_val))
+
+# pow it again and set
+sub.run([xbacklight, "-set", str(math.pow(new_backlight, 2))])
diff --git a/pkgs/profpatsch/backlight/default.nix b/pkgs/profpatsch/backlight/default.nix
new file mode 100644
index 00000000..d2f40be0
--- /dev/null
+++ b/pkgs/profpatsch/backlight/default.nix
@@ -0,0 +1,17 @@
+{ stdenv, python3, xbacklight}:
+
+stdenv.mkDerivation rec {
+  name = "backlight";
+
+  src = ./backlight.py;
+  phases = [ "installPhase" "fixupPhase" ];
+
+  buildInputs = [ python3 ];
+
+  installPhase = ''
+    install -D ${src} $out/bin/backlight
+    substituteInPlace $out/bin/backlight \
+      --replace '"xbacklight"' '"${xbacklight}/bin/xbacklight"'
+  '';
+
+}
diff --git a/pkgs/profpatsch/default.nix b/pkgs/profpatsch/default.nix
new file mode 100644
index 00000000..1bc29d57
--- /dev/null
+++ b/pkgs/profpatsch/default.nix
@@ -0,0 +1,183 @@
+{ stdenv, lib, pkgs }:
+
+let
+  inherit (pkgs) callPackage;
+
+  # Takes a derivation and a list of binary names
+  # and returns an attribute set of `name -> path`.
+  # The list can also contain renames in the form of
+  # { use, as }, which goes `as -> usePath`.
+  getBins = drv: xs:
+    let f = x:
+      # TODO: typecheck
+      let x' = if builtins.isString x then { use = x; as = x; } else x;
+      in {
+        name = x'.as;
+        value = "${lib.getBin drv}/bin/${x'.use}";
+      };
+    in builtins.listToAttrs (builtins.map f xs);
+
+  # various nix utils and fun experiments
+  nixperiments =
+    let
+      src = pkgs.fetchFromGitHub {
+        owner = "Profpatsch";
+        repo = "nixperiments";
+        rev = "519cddb867af054f242cb318ea0c61efe56a2471";
+        sha256 = "1i11yr2q40l2ghccn5lydp3dbag8m7y9vl456ghzygyz48jzavf9";
+      };
+    in import src { nixpkgs = pkgs; };
+
+  testing = import ./testing {
+    inherit stdenv lib;
+    inherit (runExeclineFns) runExecline;
+    inherit (pkgs) runCommandLocal;
+    bin = getBins pkgs.s6PortableUtils [ "s6-touch" "s6-echo" ];
+  };
+
+  # TODO: upstream
+  writeHaskellInterpret = nameOrPath: { withPackages ? lib.const [] }: content:
+    let ghc = pkgs.haskellPackages.ghcWithPackages withPackages; in
+    pkgs.writers.makeScriptWriter {
+      interpreter = "${ghc}/bin/runhaskell";
+      check = pkgs.writers.writeDash "ghc-typecheck" ''
+        ln -s "$1" ./Main.hs
+        ${ghc}/bin/ghc -fno-code -Wall ./Main.hs
+      '';
+    } nameOrPath content;
+
+  runExeclineFns =
+    # todo: factor out calling tests
+    let
+      it = import ./execline/run-execline.nix {
+        bin = getBins pkgs.execline [ "execlineb" "redirfd" "importas" "exec" ];
+        inherit stdenv lib;
+      };
+      itLocal = name: args: execline:
+        it name (args // {
+          derivationArgs = args.derivationArgs or {} // {
+            preferLocalBuild = true;
+            allowSubstitutes = false;
+          };
+        }) execline;
+
+      tests = import ./execline/run-execline-tests.nix {
+        # can’t use runExeclineLocal in the tests,
+        # because it is tested by the tests (well, it does
+        # work, but then you have to run the tests every time)
+        runExecline = it;
+        inherit (testing) drvSeqL;
+        inherit (pkgs) coreutils;
+        inherit stdenv;
+        bin = (getBins pkgs.execline [
+                 "execlineb"
+                 { use = "if"; as = "execlineIf"; }
+                 "redirfd" "importas"
+               ])
+           // (getBins pkgs.s6PortableUtils
+                [ "s6-cat" "s6-grep" "s6-touch" "s6-test" "s6-chmod" ]);
+       };
+    in {
+      runExecline = it;
+      runExeclineLocal = name: args: execline:
+        testing.drvSeqL tests (itLocal name args execline);
+    };
+
+  writeExeclineFns = import ./execline/write-execline.nix {
+    inherit pkgs;
+  };
+
+
+in rec {
+  inherit (nixperiments)
+    # filterSource by parsing a .gitignore file
+    filterSourceGitignore
+    # canonical pattern matching primitive
+    match
+    # generate an option parser for scripts
+    script
+    # derivation testing
+    drvSeq drvSeqL withTests
+    # using the nix evaluator as a json transformation runtime
+    json2json json2string;
+
+  backlight = callPackage ./backlight { inherit (pkgs.xorg) xbacklight; };
+  display-infos = callPackage ./display-infos { inherit sfttime; };
+  git-commit-index = callPackage ./git-commit-index { inherit script; };
+  nix-http-serve = callPackage ./nix-http-serve {};
+  nman = callPackage ./nman {};
+  sfttime = callPackage ./sfttime {};
+  show-qr-code = callPackage ./show-qr-code {};
+  warpspeed = callPackage ./warpspeed {
+    inherit (pkgs.haskellPackages) ghcWithPackages;
+  };
+  youtube2audiopodcast = callPackage ./youtube2audiopodcast {
+    inherit writeExecline writeHaskellInterpret getBins runInEmptyEnv sandbox;
+  };
+
+  inherit (callPackage ./utils-hs {})
+    nix-gen until watch-server
+    haskellPackages;
+
+  query-audio-streams = callPackage ./query-album-streams {
+    inherit writeExecline writeHaskellInterpret getBins;
+  };
+
+  # patched version of droopy, with javascript user-enhancement
+  droopy = pkgs.droopy.overrideDerivation (old: {
+    src = pkgs.fetchFromGitHub {
+      owner = "Profpatsch";
+      repo = "Droopy";
+      rev = "55c60c612b913f9fbce9fceebbcb3a332152f1a4";
+      sha256 = "0jcazj9gkdf4k7vsi33dpw9myyy02gjihwsy36dfqq4bas312cq1";
+    };
+    installPhase = old.installPhase or "" + ''
+      mkdir -p $out/share/droopy
+      cp -r $src/static $out/share/droopy
+    '';
+    makeWrapperArgs = old.makeWrapperArgs or [] ++ [
+      "--set DROOPY_STATIC \"$out/share/droopy/static\""
+    ];
+  });
+
+  inherit (runExeclineFns)
+    runExecline runExeclineLocal;
+  inherit (writeExeclineFns)
+    writeExecline writeExeclineBin;
+  inherit (import ./execline/runblock.nix { inherit pkgs; })
+    runblock;
+  inherit (import ./execline/e.nix { inherit pkgs writeExecline getBins; })
+    e;
+
+  inherit getBins;
+
+  inherit (import ./sandbox.nix {inherit pkgs writeExecline; })
+    sandbox runInEmptyEnv;
+
+  symlink = pkgs.callPackage ./execline/symlink.nix {
+    inherit runExecline;
+  };
+
+  importer = pkgs.callPackage ./execline/importer.nix {
+    inherit symlink;
+  };
+
+
+  easy-dhall-nix = (import (pkgs.fetchFromGitHub {
+    owner = "justinwoo";
+    repo = "easy-dhall-nix";
+    rev = "7e063af9bd6f6bd4b4be00dd0a8bff272893fc44";
+    sha256 = "0c47zlxhpjm1hddhxsqy86mkyy7hlym1rjmnapc3b7x7bkchr8za";
+  }) { inherit pkgs; });
+
+  dhall = easy-dhall-nix.dhall-simple;
+  dhall-nix = easy-dhall-nix.dhall-nix-simple;
+
+  dhall-flycheck = haskellPackages.callPackage
+    (import "${pkgs.fetchFromGitHub {
+      owner = "Profpatsch";
+      repo = "dhall-flycheck";
+      rev = "0db095732820cd27eccbe9ece97500dd292353de";
+      sha256 = "05n76b781fgm5n7kqq1gpqfzshjfpadahxryxqrfvs3sjn3a6bvp";
+    }}/dhall-flycheck.nix") {};
+}
diff --git a/pkgs/profpatsch/display-infos/default.nix b/pkgs/profpatsch/display-infos/default.nix
new file mode 100644
index 00000000..b8bb2a07
--- /dev/null
+++ b/pkgs/profpatsch/display-infos/default.nix
@@ -0,0 +1,72 @@
+{ lib, runCommandLocal, writeText, python3, libnotify, bc, sfttime }:
+
+let
+  name = "display-infos-0.1.0";
+  script = writeText (name + "-script") ''
+    #!@python3@
+
+    import sys
+    import glob
+    import subprocess as sub
+    import os.path as path
+    import statistics as st
+
+    def readint(fn):
+        with open(fn, 'r') as f:
+            return int(f.read())
+
+    def seconds_to_sft(secs):
+        p = sub.Popen(["@bc@", "-l"], stdin=sub.PIPE, stdout=sub.PIPE)
+        (sft, _) = p.communicate(input="scale=2; obase=16; {} / 86400\n".format(secs).encode())
+        p.terminate()
+        return str(sft.strip().decode())
+
+    charging = readint("/sys/class/power_supply/AC/online")
+
+    full = 0
+    now  = 0
+    # this is "to charged" if charging and "to empty" if not
+    seconds_remaining = 0
+    for bat in glob.iglob("/sys/class/power_supply/BAT*"):
+
+        # these files might be different for different ACPI/battery providers
+        # see the full list in acpi.c of the acpi(1) tool
+        # unit: who knows
+        full += readint(path.join(bat, "energy_full"))
+        now  += readint(path.join(bat, "energy_now" ))
+        # in unit?/hours, hopefully the same unit as above
+        # ACPI is a garbage fire
+        current_rate = readint(path.join(bat, "power_now"))
+
+        if current_rate == 0:
+          continue
+        elif charging:
+          seconds_remaining += 3600 * (full - now) / current_rate
+        else:
+          seconds_remaining += 3600 * now / current_rate
+
+    bat = round( now/full, 2 )
+    ac = "🗲 " if charging else ""
+    sft_remaining = seconds_to_sft(seconds_remaining)
+    date = sub.run(["date", "+%d.%m. %a %T"], stdout=sub.PIPE).stdout.strip().decode()
+    sftdate = sub.run(["@sfttime@"], stdout=sub.PIPE).stdout.strip().decode()
+    notify = "BAT: {percent}% {ac}{charge}| {date} | {sftdate}".format(
+      percent = int(bat*100),
+      ac = ac,
+      charge = "{} ".format(sft_remaining) if seconds_remaining else "",
+      date = date,
+      sftdate = sftdate
+    )
+    print(notify)
+  '';
+
+in
+  with lib; runCommandLocal "display-infos" {
+    meta.description = "Script to display time & battery";
+  } ''
+    substitute ${script} script \
+      --replace "@python3@" "${getBin python3}/bin/python3" \
+      --replace "@bc@" "${getBin bc}/bin/bc" \
+      --replace "@sfttime@" "${getBin sfttime}/bin/sfttime"
+    install -D script $out/bin/display-infos
+  ''
diff --git a/pkgs/profpatsch/execline/e.nix b/pkgs/profpatsch/execline/e.nix
new file mode 100644
index 00000000..ca72c6f0
--- /dev/null
+++ b/pkgs/profpatsch/execline/e.nix
@@ -0,0 +1,24 @@
+{ writeExecline, getBins, pkgs }:
+let
+
+  bins = getBins pkgs.rlwrap [ "rlwrap" ]
+    // getBins pkgs.s6-portable-utils [ { use = "s6-cat"; as = "cat"; } ]
+    // getBins pkgs.execline [ "execlineb" ];
+
+  # minimal execline shell
+  e =
+    let
+      prompt = [ "if" [ "printf" ''\e[0;33me>\e[0m '' ] ];
+    in
+      writeExecline "e" {} ([
+        bins.rlwrap
+          "--remember"
+          "--quote-characters" "\""
+          "--complete-filenames"
+      ] ++ prompt ++ [
+        "forstdin" "-d\n" "cmd"
+        "importas" "cmd" "cmd"
+        "foreground" [ bins.execlineb "-Pc" "$cmd" ]
+      ] ++ prompt);
+
+in { inherit e; }
diff --git a/pkgs/profpatsch/execline/escape.nix b/pkgs/profpatsch/execline/escape.nix
new file mode 100644
index 00000000..dff07a19
--- /dev/null
+++ b/pkgs/profpatsch/execline/escape.nix
@@ -0,0 +1,31 @@
+{ lib }:
+let
+  # replaces " and \ to \" and \\ respectively and quote with "
+  # e.g.
+  #   a"b\c -> "a\"b\\c"
+  #   a\"bc -> "a\\\"bc"
+  # TODO upsteam into nixpkgs
+  escapeExeclineArg = arg:
+    ''"${builtins.replaceStrings [ ''"'' ''\'' ] [ ''\"'' ''\\'' ] (toString arg)}"'';
+
+  # Escapes an execline (list of execline strings) to be passed to execlineb
+  # Give it a nested list of strings. Nested lists are interpolated as execline
+  # blocks ({}).
+  # Everything is quoted correctly.
+  #
+  # Example:
+  #   escapeExecline [ "if" [ "somecommand" ] "true" ]
+  #   == ''"if" { "somecommand" } "true"''
+  escapeExecline = execlineList: lib.concatStringsSep " "
+    (let
+      go = arg:
+        if      builtins.isString arg then [(escapeExeclineArg arg)]
+        else if builtins.isPath arg then [(escapeExeclineArg "${arg}")]
+        else if lib.isDerivation arg then [(escapeExeclineArg arg)]
+        else if builtins.isList arg then [ "{" ] ++ builtins.concatMap go arg ++ [ "}" ]
+        else abort "escapeExecline can only hande nested lists of strings, was ${lib.generators.toPretty {} arg}";
+     in builtins.concatMap go execlineList);
+
+in {
+  inherit escapeExecline;
+}
diff --git a/pkgs/profpatsch/execline/importer.nix b/pkgs/profpatsch/execline/importer.nix
new file mode 100644
index 00000000..67464d17
--- /dev/null
+++ b/pkgs/profpatsch/execline/importer.nix
@@ -0,0 +1,45 @@
+{ lib, coreutils, s6-portable-utils, symlink }:
+let
+  example = {from, as, just, ...}:
+    [
+      (from coreutils [
+        (just "echo")
+        (as "core-ls" "ls")
+      ])
+      (from s6-portable-utils [
+        (as "ls" "s6-ls")
+        (just "s6-echo")
+      ])
+    ];
+
+  runImport = impsFn:
+    let
+      combinators = rec {
+        from = source: imports: {
+          inherit source imports;
+        };
+        as = newname: oldname: {
+          inherit oldname newname;
+        };
+        just = x: as x x;
+      };
+
+      # Drv -> As -> Symlink
+      toBin = module: {oldname, newname}: {
+        dest = "bin/${newname}";
+        orig = "${module}/bin/${oldname}";
+      };
+      # List (Import { source: Drv
+      #              , imports: List (As { oldname: String
+      #                                  , newname: String }))
+      # -> Drv
+      run = imps:
+        symlink "foo" (lib.concatLists
+          (map ({source, imports}:
+                   map (toBin source) imports)
+               imps));
+
+    # TODO: typecheck w/ newtypes
+    in run (impsFn combinators);
+
+in runImport example
diff --git a/pkgs/profpatsch/execline/run-execline-tests.nix b/pkgs/profpatsch/execline/run-execline-tests.nix
new file mode 100644
index 00000000..c3f534cc
--- /dev/null
+++ b/pkgs/profpatsch/execline/run-execline-tests.nix
@@ -0,0 +1,89 @@
+{ stdenv, drvSeqL, runExecline, bin
+# https://www.mail-archive.com/skaware@list.skarnet.org/msg01256.html
+, coreutils }:
+
+let
+
+  # lol
+  writeScript = name: script: runExecline name {
+    derivationArgs = {
+      inherit script;
+      passAsFile = [ "script" ];
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+    };
+  } [
+      "importas" "-ui" "s" "scriptPath"
+      "importas" "-ui" "out" "out"
+      "foreground" [
+        "${coreutils}/bin/mv" "$s" "$out"
+      ]
+      "${bin.s6-chmod}" "0755" "$out"
+  ];
+
+  # execline block of depth 1
+  block = args: builtins.map (arg: " ${arg}") args ++ [ "" ];
+
+  # derivation that tests whether a given line exists
+  # in the given file. Does not use runExecline, because
+  # that should be tested after all.
+  fileHasLine = line: file: derivation {
+    name = "run-execline-test-file-${file.name}-has-line";
+    inherit (stdenv) system;
+    builder = bin.execlineIf;
+    args =
+      (block [
+        bin.redirfd "-r" "0" file   # read file to stdin
+        bin.s6-grep "-F" "-q" line   # and grep for the line
+      ])
+      ++ [
+        # if the block succeeded, touch $out
+        bin.importas "-ui" "out" "out"
+        bin.s6-touch "$out"
+      ];
+    preferLocalBuild = true;
+    allowSubstitutes = false;
+  };
+
+  # basic test that touches out
+  basic = runExecline "run-execline-test-basic" {
+    derivationArgs = {
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+    };
+  } [
+      "importas" "-ui" "out" "out"
+      "${bin.s6-touch}" "$out"
+  ];
+
+  # whether the stdin argument works as intended
+  stdin = fileHasLine "foo" (runExecline "run-execline-test-stdin" {
+    stdin = "foo\nbar\nfoo";
+    derivationArgs = {
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+    };
+  } [
+      "importas" "-ui" "out" "out"
+      # this pipes stdout of s6-cat to $out
+      # and s6-cat redirects from stdin to stdout
+      "redirfd" "-w" "1" "$out" bin.s6-cat
+  ]);
+
+  wrapWithVar = runExecline "run-execline-test-wrap-with-var" {
+    builderWrapper = writeScript "var-wrapper" ''
+      #!${bin.execlineb} -S0
+      export myvar myvalue $@
+    '';
+    derivationArgs = {
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+    };
+  } [
+    "importas" "-ui" "v" "myvar"
+    "if" [ bin.s6-test "myvalue" "=" "$v" ]
+      "importas" "out" "out"
+      bin.s6-touch "$out"
+  ];
+
+in [ basic stdin wrapWithVar ]
diff --git a/pkgs/profpatsch/execline/run-execline.nix b/pkgs/profpatsch/execline/run-execline.nix
new file mode 100644
index 00000000..2efe43d6
--- /dev/null
+++ b/pkgs/profpatsch/execline/run-execline.nix
@@ -0,0 +1,71 @@
+{ stdenv, bin, lib }:
+name:
+{
+# a string to pass as stdin to the execline script
+stdin ? ""
+# a program wrapping the acutal execline invocation;
+# should be in Bernstein-chaining style
+, builderWrapper ? bin.exec
+# additional arguments to pass to the derivation
+, derivationArgs ? {}
+}:
+# the execline script as a nested list of string,
+# representing the blocks;
+# see docs of `escapeExecline`.
+ execline:
+
+# those arguments can’t be overwritten
+assert !derivationArgs ? system;
+assert !derivationArgs ? name;
+assert !derivationArgs ? builder;
+assert !derivationArgs ? args;
+
+derivation (derivationArgs // {
+  # TODO: what about cross?
+  inherit (stdenv) system;
+  inherit name;
+
+  # okay, `builtins.toFile` does not accept strings
+  # that reference drv outputs. This means we need
+  # to pass the script and stdin as envvar;
+  # this might clash with another passed envar,
+  # so we give it a long & unique name
+  _runExeclineScript =
+    let
+      escape = (import ./escape.nix { inherit lib; });
+    in escape.escapeExecline execline;
+  _runExeclineStdin = stdin;
+  passAsFile = [
+    "_runExeclineScript"
+    "_runExeclineStdin"
+  ] ++ derivationArgs.passAsFile or [];
+
+  # the default, exec acts as identity executable
+  builder = builderWrapper;
+
+  args = [
+    bin.importas             # import script file as $script
+    "-ui"                    # drop the envvar afterwards
+    "script"                 # substitution name
+    "_runExeclineScriptPath" # passed script file
+
+    # TODO: can we scrap stdin via builderWrapper?
+    bin.importas             # do the same for $stdin
+    "-ui"
+    "stdin"
+    "_runExeclineStdinPath"
+
+    bin.redirfd              # now we
+    "-r"                     # read the file
+    "0"                      # into the stdin of execlineb
+    "$stdin"                 # that was given via stdin
+
+    bin.execlineb            # the actual invocation
+    # TODO: depending on the use-case, -S0 might not be enough
+    # in all use-cases, then a wrapper for execlineb arguments
+    # should be added (-P, -S, -s).
+    "-S0"                    # set $@ inside the execline script
+    "-W"                     # die on syntax error
+    "$script"                # substituted by importas
+  ];
+})
diff --git a/pkgs/profpatsch/execline/runblock.nix b/pkgs/profpatsch/execline/runblock.nix
new file mode 100644
index 00000000..0b380229
--- /dev/null
+++ b/pkgs/profpatsch/execline/runblock.nix
@@ -0,0 +1,69 @@
+{ pkgs }:
+let
+
+  # This is a rewrite of execline’s runblock.
+  # It adds the feature that instead of just
+  # executing the block it reads, it can also
+  # pass it as argv to given commands.
+  #
+  # This is going to be added to a future version
+  # of execline by skarnet, but for now it’s easier
+  # to just dirtily reimplement it in Python.
+  runblock = pkgs.writers.writePython3 "runblock" {} ''
+    import sys
+    import os
+
+    skip = False
+    one = sys.argv[1]
+    if one == "-r":
+        skip = True
+        block_number = int(sys.argv[2])
+        block_start = 3
+    elif one.startswith("-"):
+        print("only -r supported", file=sys.stderr)
+        sys.exit(100)
+    else:
+        block_number = int(one)
+        block_start = 2
+
+    execline_argv_no = int(os.getenvb(b"#"))
+    runblock_argv = [os.getenv(str(no)) for no in range(1, execline_argv_no + 1)]
+
+
+    def parse_block(args):
+        new_args = []
+        for arg in args:
+            if arg == "":
+                break
+            elif arg.startswith(" "):
+                new_args.append(arg[1:])
+            else:
+                print(
+                  "unterminated block: {}".format(args),
+                  file=sys.stderr
+                )
+                sys.exit(100)
+        args_rest = args[len(new_args)+1:]
+        return (new_args, args_rest)
+
+
+    if skip:
+        rest = runblock_argv
+        for _ in range(0, block_number-1):
+            (_, rest) = parse_block(rest)
+        new_argv = rest
+    else:
+        new_argv = []
+        rest = runblock_argv
+        for _ in range(0, block_number):
+            (new_argv, rest) = parse_block(rest)
+
+    given_argv = sys.argv[block_start:]
+    run = given_argv + new_argv
+    os.execvp(
+        file=run[0],
+        args=run
+    )
+  '';
+
+in { inherit runblock; }
diff --git a/pkgs/profpatsch/execline/symlink.nix b/pkgs/profpatsch/execline/symlink.nix
new file mode 100644
index 00000000..c6a311d8
--- /dev/null
+++ b/pkgs/profpatsch/execline/symlink.nix
@@ -0,0 +1,46 @@
+{ lib, s6-portable-utils, coreutils, runExecline }:
+# DrvPath :: path relative to the derivation
+# AbsPath :: absolute path in the store
+#    Name
+# -> List (Symlink { dest: DrvPath, orig: AbsPath })
+# -> Drv
+name: links:
+
+let
+  toNetstring = s:
+    "${toString (builtins.stringLength s)}:${s},";
+
+in
+runExecline name {
+  derivationArgs = {
+    pathTuples = lib.concatMapStrings
+      ({dest, orig}: toNetstring
+        (toNetstring dest + (toNetstring orig)))
+      links;
+    passAsFile = [ "pathTuples" ];
+    # bah! coreutils just for cat :(
+    PATH = lib.makeBinPath [ s6-portable-utils ];
+  };
+} [
+  "importas" "-ui" "p" "pathTuplesPath"
+  "importas" "-ui" "out" "out"
+  "forbacktickx" "-d" "" "destorig" [ "${coreutils}/bin/cat" "$p" ]
+    "importas" "-ui" "do" "destorig"
+    "multidefine" "-d" "" "$do" [ "destsuffix" "orig" ]
+    "define" "dest" ''''${out}/''${destsuffix}''
+
+    # this call happens for every file, not very efficient
+    "foreground" [
+      "backtick" "-n" "d" [ "s6-dirname" "$dest" ]
+      "importas" "-ui" "d" "d"
+      "s6-mkdir" "-p" "$d"
+    ]
+
+    "ifthenelse" [ "s6-test" "-L" "$orig" ] [
+      "backtick" "-n" "res" [ "s6-linkname" "-f" "$orig" ]
+      "importas" "-ui" "res" "res"
+      "s6-ln" "-fs" "$res" "$dest"
+    ] [
+      "s6-ln" "-fs" "$orig" "$dest"
+    ]
+]
diff --git a/pkgs/profpatsch/execline/write-execline.nix b/pkgs/profpatsch/execline/write-execline.nix
new file mode 100644
index 00000000..23c96454
--- /dev/null
+++ b/pkgs/profpatsch/execline/write-execline.nix
@@ -0,0 +1,35 @@
+{ pkgs }:
+let
+  escape = import ./escape.nix { inherit (pkgs) lib; };
+
+  # Write a list of execline argv parameters to an execline script.
+  # Everything is escaped correctly.
+  # TODO upstream into nixpkgs
+  writeExeclineCommon = writer: name: {
+     # "var": substitute readNArgs variables and start $@ from the (readNArgs+1)th argument
+     # "var-full": substitute readNArgs variables and start $@ from $0
+     # "env": don’t substitute, set # and 0…n environment vaariables, where n=$#
+     # "none": don’t substitute or set any positional arguments
+     # "env-no-push": like "env", but bypass the push-phase. Not recommended.
+     argMode ? "var",
+     # Number of arguments to be substituted as variables (passed to "var"/"-s" or "var-full"/"-S"
+     readNArgs ? 0,
+  }: argList:
+   let
+     env =
+       if      argMode == "var" then "s${toString readNArgs}"
+       else if argMode == "var-full" then "S${toString readNArgs}"
+       else if argMode == "env" then ""
+       else if argMode == "none" then "P"
+       else if argMode == "env-no-push" then "p"
+       else abort ''"${toString argMode}" is not a valid argMode, use one of "var", "var-full", "env", "none", "env-no-push".'';
+   in writer name ''
+    #!${pkgs.execline}/bin/execlineb -W${env}
+    ${escape.escapeExecline argList}
+  '';
+  writeExecline = writeExeclineCommon pkgs.writeScript;
+  writeExeclineBin = writeExeclineCommon pkgs.writeScriptBin;
+
+in {
+  inherit writeExecline writeExeclineBin;
+}
diff --git a/pkgs/profpatsch/git-commit-index/default.nix b/pkgs/profpatsch/git-commit-index/default.nix
new file mode 100644
index 00000000..d7b9a781
--- /dev/null
+++ b/pkgs/profpatsch/git-commit-index/default.nix
@@ -0,0 +1,69 @@
+{ cdb, git, writeShellScriptBin, symlinkJoin, lib, script, runCommandLocal }:
+
+let
+  pathPlus = ''PATH="$PATH:${lib.makeBinPath [cdb git]}"'';
+
+  mkIndex = script.withOptions {
+    name = "git-commit-index-create";
+    synopsis = "Create a git commit index for all .git directories, recursively.";
+
+    options = {
+      index = {
+        description = "Location of the index. Must not exist.";
+        checks = [ script.optionChecks.emptyPath ];
+      };
+    };
+
+    extraArgs = {
+      description = "List of root directories to recursively search";
+      name = "DIRS";
+      checks = [ script.argsChecks.allAreDirs ];
+    };
+
+    script = ''
+      ${pathPlus}
+      source ${./lib.sh}
+      mkdir "$index"
+      for root in "$@"; do
+        for repo in $(findRepos "$root"); do
+          genIndex "$index" "$repo"
+        done
+      done
+    '';
+  };
+
+  queryIndex = script.withOptions {
+    name = "git-commit-index-query";
+
+    synopsis = "Search a git commit index for a rev, return the repository that rev is in.";
+
+    options = {
+      index = {
+        description = "Location of the populated index.";
+        checks = [ script.optionChecks.isDir ];
+      };
+      rev = {
+        description = "Full git rev hash to look up";
+        # TODO: check that it is a commit hash?
+        checks = [];
+      };
+    };
+
+    script = ''
+      ${pathPlus}
+      source ${./lib.sh}
+
+      query "$index" "$rev"
+    '';
+  };
+
+  # magitOpenCommit = writeShellScriptBin "git-commit-index-magit-open" ''
+  #   ${pathPlus}
+# emacsclient -e '(let ((default-directory "/home/philip/kot/work/tweag/lorri"))
+#                                    (magit-mode-setup #\'magit-revision-mode "43a72bc220dae8f34bd0d889f2b1b1ce2a6092b7" nil nil nil))'
+
+in
+  runCommandLocal "git-commit-index" {} ''
+    install -D ${mkIndex} $out/bin/git-commit-index-create
+    install -D ${queryIndex} $out/bin/git-commit-index-query
+  ''
diff --git a/pkgs/profpatsch/git-commit-index/lib.sh b/pkgs/profpatsch/git-commit-index/lib.sh
new file mode 100644
index 00000000..229eec1e
--- /dev/null
+++ b/pkgs/profpatsch/git-commit-index/lib.sh
@@ -0,0 +1,102 @@
+set -euo pipefail
+
+# nix-shell -p cdb --run 'bash -c \'source ~/tmp/gitcdb.sh; for r in $(findRepos ~/kot); do genIndex . "$r"; done\''
+
+findRepos () {
+    find "$1" -type d -name ".git"
+    # TODO check each repo is actually git repo
+}
+
+genIndex () {
+    local indexDir=$(realpath -- "$1")
+    local path=$(realpath -- "$2")
+    if [ ! -d "$indexDir" ]; then
+        echo "index directory does not exist: $indexDir"
+        exit 111
+    fi
+    # TODO: multimap failure
+    #
+    local filename="$indexDir/$(echo $path | sed -e 's|_|__|g' -e 's|/|_|g')"
+    local pathLength=$(echo "$path" | wc --bytes | tr -d '\n')
+    (pushd "$path" > /dev/null \
+            && ( git log --all --format="format:%H" \
+                     | sed -e "s/^\(.*\)$/+40,0:\1->/" \
+               ; echo \
+               ; echo "+8,${pathLength}:git-path->${path}" \
+               ; echo; echo \
+               ) \
+            | cdbmake "$filename" "$filename.tmp" \
+    )
+}
+
+query () {
+    local indexDir=$(realpath -- "$1")
+    local key="$2"
+
+    local found=0
+    local result=
+
+    # TODO make this parallel (and switch away from bash)
+    for f in "$indexDir"/*; do
+
+        set +e
+        # don't need result because empty string
+        <"$f" cdbget "$key" >/dev/null
+        local ret=$?
+        set -e
+
+        case $ret in
+            0)
+                # TODO: back
+                found=1
+
+                set +e
+                # now find original path
+                local origDotGit=$(<"$f" cdbget "git-path")
+                local retGitPath=$?
+                set -e
+
+                case $retGitPath in
+                    0)
+                        :
+                        ;;
+                    100)
+                        echo "shouldn’t happen; git-path was not in $f"
+                        exit 127
+                        ;;
+                    111)
+                        echo "db error in $f"
+                        exit 111
+                        ;;
+                    *)
+                        echo "shouldn’t happen; exitcode was $ret"
+                        exit 127
+                        ;;
+                esac
+
+                # return workspace file
+                result=$(dirname "$origDotGit")
+                break
+                ;;
+            100)
+                # not found
+                :
+                ;;
+            111)
+                echo "db error in $f"
+                exit 111
+                ;;
+            *)
+                echo "shouldn’t happen; exitcode was $ret"
+                exit 127
+                ;;
+        esac
+    done
+
+    if [ $found -eq 0 ]; then
+        exit 100
+    else
+        echo "$result"
+        exit 0
+    fi
+}
diff --git a/pkgs/profpatsch/nix-http-serve/default.nix b/pkgs/profpatsch/nix-http-serve/default.nix
new file mode 100644
index 00000000..be0d8b23
--- /dev/null
+++ b/pkgs/profpatsch/nix-http-serve/default.nix
@@ -0,0 +1,11 @@
+{ stdenv, pkgs, fetchFromGitHub }:
+
+let
+  src = fetchFromGitHub {
+    owner = "Profpatsch";
+    repo = "nix-http-serve";
+    rev = "f1f188da4a78c3d359cc1a92663d82ee8c6acd2f";
+    sha256 = "12qfpzhij0si2p5p8d1iri1iz2kv2bn3jsvqbw32x9gfl728i1bi";
+  };
+
+in (pkgs.callPackage src {}).nix-http-serve
diff --git a/pkgs/profpatsch/nman/default.nix b/pkgs/profpatsch/nman/default.nix
new file mode 100644
index 00000000..96699833
--- /dev/null
+++ b/pkgs/profpatsch/nman/default.nix
@@ -0,0 +1,14 @@
+{ lib, runCommand, go }:
+
+runCommand "nman" {
+  meta = with lib; {
+    description = "Invoke manpage in temporary nix-shell";
+    license = licenses.gpl3;
+  };
+} ''
+    mkdir cache
+    env GOCACHE="$PWD/cache" \
+      ${lib.getBin go}/bin/go build -o nman ${./nman.go}
+    install -D nman $out/bin/nman
+''
+
diff --git a/pkgs/profpatsch/nman/nman.go b/pkgs/profpatsch/nman/nman.go
new file mode 100644
index 00000000..e6b6247d
--- /dev/null
+++ b/pkgs/profpatsch/nman/nman.go
@@ -0,0 +1,137 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"log"
+	"bytes"
+	"os/exec"
+	"io/ioutil"
+	"syscall"
+)
+
+var usage = `nman drvAttr [section|page] [page]
+
+Open man pages in a temporary nix-shell.
+1 If one argument is given, the drvAttr & page have the same name.
+2 If two arguments are given and the second arg is
+    a <number>) like 1, but in the man section <number>
+    a <page>  ) like 1, but open the page <page>
+3 If three arguments are given, the order is <drvAttr> <sect> <page>
+`
+
+func lookupManPage(manpath string, manSection int64, manPage string) (int, error) {
+	if manSection < -1 {
+		panic("manSection must not be < -1")
+	}
+	// holy debug printf
+	// fmt.Printf("attr: %s, sect: %d, page: %s\n", drvAttr, manSection, manPage)
+
+	manBin, err := exec.LookPath("man");
+	if err != nil {
+		return 0, fmt.Errorf("man executable not in PATH")
+	}
+
+	var manArgs []string
+	if (manSection == -1) {
+		manArgs = []string{manPage}
+	} else {
+		manArgs = []string{strconv.FormatInt(manSection, 10), manPage}
+	}
+
+	man := exec.Command(manBin, manArgs...)
+
+        man.Env = append(
+		os.Environ(),
+		fmt.Sprintf("MANPATH=%s", manpath))
+	man.Stderr = os.Stderr
+	man.Stdout = os.Stdout
+	err = man.Run()
+	if exiterr, ok := err.(*exec.ExitError); ok {
+		ws := exiterr.Sys().(syscall.WaitStatus)
+		return ws.ExitStatus(), nil
+	} else {
+		return 0, err
+	}
+
+}
+
+func buildManOutput(drvAttr string) (string, error) {
+	nixExpr := fmt.Sprintf(
+		`with import <nixpkgs> {}; %s.man or %s.doc or %s.out or %s`,
+		drvAttr, drvAttr, drvAttr, drvAttr)
+
+	nixBuild := exec.Command("nix-build", "-E", nixExpr)
+	nixBuild.Stderr = os.Stderr
+	pipe, err := nixBuild.StdoutPipe()
+	if err != nil { return "", fmt.Errorf("could not access stdout of nix-build: %s", err) }
+
+	err = nixBuild.Start()
+	if err != nil { return "", fmt.Errorf("could not start nix-build: %s", err) }
+
+	out, err := ioutil.ReadAll(pipe)
+	if err != nil { return "", fmt.Errorf("could not read from nix-build: %s", err) }
+
+	lines := bytes.Split(bytes.TrimSpace(out), []byte("\n"))
+	err = nixBuild.Wait()
+	if err != nil { return "", fmt.Errorf("nix-build process died: %s", err) }
+
+	// last line ouf output looks like: /nix/store/abc…-foobar.drv!output
+	drvPath := bytes.Split(lines[len(lines)-1], []byte("!"))[0]
+	if _, err := os.Stat(string(drvPath)); err != nil {
+		return "", fmt.Errorf("%s doesn’t look like an output path: %s", drvPath, err)
+	}
+
+	return string(drvPath), nil
+}
+
+func main() {
+	var manPage string
+	var drvAttr string
+
+	args := os.Args
+
+	// man section or -1 if no man section
+        var manSection int64 = -1
+	if (len(args) >= 3) {
+		i, err := strconv.ParseUint(args[2], 10, 64)
+		if err == nil && i >= 0 {
+			manSection = int64(i)
+		}
+	}
+
+	// the first argument is always a derivation attribute
+	switch len(args) {
+	case 2:
+		// arg is package and drv attr
+		manPage = args[1]
+	case 3:
+		if (manSection == -1) {
+			// like 2, but arg 2 is package
+			manPage = args[2]
+		} else {
+			// man section given, page is arg 1
+			manPage = args[1]
+		}
+	case 4:
+		// arg 2 is manSection, arg 3 is package
+		manPage = args[3]
+	default:
+		fmt.Print(usage)
+		os.Exit(-1)
+	}
+
+	drvAttr = args[1]
+	drvPath, err := buildManOutput(drvAttr)
+	if err != nil {
+		log.Fatal(err)
+	}
+	exitStatus, err := lookupManPage(drvPath + "/share/man", manSection, manPage)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	os.Exit(exitStatus)
+
+}
diff --git a/pkgs/profpatsch/query-album-streams/default.nix b/pkgs/profpatsch/query-album-streams/default.nix
new file mode 100644
index 00000000..249676cb
--- /dev/null
+++ b/pkgs/profpatsch/query-album-streams/default.nix
@@ -0,0 +1,3 @@
+{ pkgs, writeExecline, getBins, writeHaskellInterpret }@attrs:
+
+import ./last-fm-api.nix attrs
diff --git a/pkgs/profpatsch/query-album-streams/last-fm-api.nix b/pkgs/profpatsch/query-album-streams/last-fm-api.nix
new file mode 100644
index 00000000..920c2bfb
--- /dev/null
+++ b/pkgs/profpatsch/query-album-streams/last-fm-api.nix
@@ -0,0 +1,87 @@
+{ pkgs, getBins, writeExecline, writeHaskellInterpret }:
+let
+
+  lib = pkgs.lib;
+  bins = getBins pkgs.httpie [ "http" ];
+
+  develop = true;
+
+  writeHaskell = name: { interpret ? false, withPackages }:
+    if interpret
+    then writeHaskellInterpret name { inherit withPackages; }
+    else pkgs.writers.writeHaskell name { libraries = withPackages pkgs.haskellPackages; };
+
+  # see https://hackage.haskell.org/package/aeson-schema-0.4.1.2/docs/Data-Aeson-Schema-Types.html#t:Schema
+  # for the schema format.
+  # The input is a json-encoding of that via quasi-quoter.
+  json-schema-validator = name: schema: writeHaskell name {
+    withPackages = hps: [ (pkgs.haskell.lib.doJailbreak (pkgs.haskell.lib.markUnbroken hps.aeson-schema)) hps.aeson ];
+    interpret = develop;
+  } ''
+    {-# language QuasiQuotes #-}
+    import qualified Data.ByteString.Lazy as BS
+    import Data.Aeson (eitherDecode')
+    import Data.Aeson.Schema
+    import System.Exit (die, exitSuccess)
+
+    main :: IO ()
+    main = do
+      stdin <- BS.getContents
+      case (eitherDecode' stdin) of
+        Left errs ->  die errs
+        Right json -> do
+          let val = validate mempty schema json
+          if val == []
+          then BS.putStr stdin >> exitSuccess
+          else die (show val)
+
+    schema :: Schema ()
+    schema = [schemaQQ| ${lib.generators.toJSON {} schema} |]
+  '';
+
+  query-lastFm-album = writeExecline "query-lastFm-album" { readNArgs = 2; } [
+    bins.http
+      "GET"
+      "https://ws.audioscrobbler.com/2.0/"
+      "--"
+      "api_key==\${1}"
+      "method==album.search"
+      "album==\${2}"
+      "format==json"
+  ];
+
+  schema = {
+    obj = { required ? true }: propsMap: {
+      type = "object";
+      inherit required;
+      properties = propsMap;
+    };
+
+    arr = { required ? true }: itemsSchema: {
+      type = "array";
+      inherit required;
+      items = itemsSchema;
+    };
+  };
+
+  validator = json-schema-validator "last-fm-album-output" (with schema; obj {} {
+    results = obj {} {
+      albummatches = obj {} {
+        album = arr {} (obj {} {
+          name = { type = "string"; };
+          artist = { type = "string"; };
+        });
+      };
+    };
+  });
+
+  query-and-validate = writeExecline "validate-lastfm-album-response" { } [
+    "pipeline" [ query-lastFm-album "$@" ]
+    "if" [ validator ]
+  ];
+
+
+# in validator
+in {
+  inherit query-lastFm-album validator query-and-validate;
+}
diff --git a/pkgs/profpatsch/s6/dhall/ServiceDirectory/default-type.dhall b/pkgs/profpatsch/s6/dhall/ServiceDirectory/default-type.dhall
new file mode 100644
index 00000000..b560ab0e
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/ServiceDirectory/default-type.dhall
@@ -0,0 +1,17 @@
+{ finish :
+	Optional Text
+, up :
+	Bool
+, setid :
+	Bool
+, notification-fd :
+	Optional Natural
+, timeout-kill :
+	Natural
+, timeout-finish :
+	Natural
+, max-death-tally :
+	Natural
+, down-signal :
+	./../imports/Signal/type.dhall
+}
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/ServiceDirectory/default.dhall b/pkgs/profpatsch/s6/dhall/ServiceDirectory/default.dhall
new file mode 100644
index 00000000..c41fff5c
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/ServiceDirectory/default.dhall
@@ -0,0 +1,20 @@
+let Signal = ./../imports/Signal.dhall
+
+in    { finish =
+		  None Text
+	  , up =
+		  True
+	  , setid =
+		  True
+	  , notification-fd =
+		  None Natural
+	  , timeout-kill =
+		  0
+	  , timeout-finish =
+		  5000
+	  , max-death-tally =
+		  100
+	  , down-signal =
+		  Signal.SIGTERM
+	  }
+	: ./default-type.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/ServiceDirectory/type.dhall b/pkgs/profpatsch/s6/dhall/ServiceDirectory/type.dhall
new file mode 100644
index 00000000..5ca3b407
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/ServiceDirectory/type.dhall
@@ -0,0 +1 @@
+./default-type.dhall â©“ { run : Text }
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/defaults.dhall b/pkgs/profpatsch/s6/dhall/defaults.dhall
new file mode 100644
index 00000000..92ca44a5
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/defaults.dhall
@@ -0,0 +1 @@
+{ ServiceDirectory = ./ServiceDirectory/default.dhall }
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/imports/Signal.dhall b/pkgs/profpatsch/s6/dhall/imports/Signal.dhall
new file mode 100644
index 00000000..b1ef0ad5
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/imports/Signal.dhall
@@ -0,0 +1 @@
+./../unix/Signal/x86.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/imports/Signal/type.dhall b/pkgs/profpatsch/s6/dhall/imports/Signal/type.dhall
new file mode 100644
index 00000000..2c45db9d
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/imports/Signal/type.dhall
@@ -0,0 +1 @@
+./../../unix/Signal/type.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/types.dhall b/pkgs/profpatsch/s6/dhall/types.dhall
new file mode 100644
index 00000000..7223df4b
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/types.dhall
@@ -0,0 +1 @@
+{ ServiceDirectory = ./ServiceDirectory/type.dhall }
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/alpha.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/alpha.dhall
new file mode 100644
index 00000000..f244a954
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/alpha.dhall
@@ -0,0 +1 @@
+./common/alpha-sparc.dhall ⫽ ./misc/alpha-sparc.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/arm.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/arm.dhall
new file mode 100644
index 00000000..35678618
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/arm.dhall
@@ -0,0 +1 @@
+./x86.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/common/alpha-sparc.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/common/alpha-sparc.dhall
new file mode 100644
index 00000000..cebdc49c
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/common/alpha-sparc.dhall
@@ -0,0 +1,35 @@
+	./standard.dhall
+  ⫽ { SIGUSR1 =
+		30
+	, SIGUSR2 =
+		31
+	, SIGCHLD =
+		20
+	, SIGCONT =
+		19
+	, SIGSTOP =
+		17
+	, SIGTSTP =
+		18
+	, SIGTTIN =
+		21
+	, SIGTTOU =
+		22
+	, SIGBUS =
+		10
+	, SIGPOLL =
+		23
+	, SIGPROF =
+		27
+	, SIGSYS =
+		12
+	, SIGURG =
+		16
+	, SIGVTALRM =
+		26
+	, SIGXCPU =
+		24
+	, SIGXFSZ =
+		25
+	}
+: ./type.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/common/constants.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/common/constants.dhall
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/common/constants.dhall
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/common/mips.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/common/mips.dhall
new file mode 100644
index 00000000..be70924e
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/common/mips.dhall
@@ -0,0 +1,35 @@
+	./standard.dhall
+  ⫽ { SIGUSR1 =
+		16
+	, SIGUSR2 =
+		17
+	, SIGCHLD =
+		18
+	, SIGCONT =
+		25
+	, SIGSTOP =
+		23
+	, SIGTSTP =
+		24
+	, SIGTTIN =
+		26
+	, SIGTTOU =
+		27
+	, SIGBUS =
+		10
+	, SIGPOLL =
+		22
+	, SIGPROF =
+		29
+	, SIGSYS =
+		12
+	, SIGURG =
+		21
+	, SIGVTALRM =
+		28
+	, SIGXCPU =
+		30
+	, SIGXFSZ =
+		31
+	}
+: ./type.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/common/standard.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/common/standard.dhall
new file mode 100644
index 00000000..7836bf10
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/common/standard.dhall
@@ -0,0 +1,25 @@
+{ SIGHUP =
+	1
+, SIGINT =
+	2
+, SIGQUIT =
+	3
+, SIGILL =
+	4
+, SIGTRAP =
+	5
+, SIGABRT =
+	6
+, SIGFPE =
+	8
+, SIGKILL =
+	9
+, SIGSEGV =
+	11
+, SIGPIPE =
+	13
+, SIGALRM =
+	14
+, SIGTERM =
+	15
+}
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/common/type.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/common/type.dhall
new file mode 100644
index 00000000..76ecb01d
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/common/type.dhall
@@ -0,0 +1,63 @@
+let POSIX1990 =
+	  { SIGHUP :
+		  Natural
+	  , SIGINT :
+		  Natural
+	  , SIGQUIT :
+		  Natural
+	  , SIGILL :
+		  Natural
+	  , SIGABRT :
+		  Natural
+	  , SIGFPE :
+		  Natural
+	  , SIGKILL :
+		  Natural
+	  , SIGSEGV :
+		  Natural
+	  , SIGPIPE :
+		  Natural
+	  , SIGALRM :
+		  Natural
+	  , SIGTERM :
+		  Natural
+	  , SIGUSR1 :
+		  Natural
+	  , SIGUSR2 :
+		  Natural
+	  , SIGCHLD :
+		  Natural
+	  , SIGCONT :
+		  Natural
+	  , SIGSTOP :
+		  Natural
+	  , SIGTSTP :
+		  Natural
+	  , SIGTTIN :
+		  Natural
+	  , SIGTTOU :
+		  Natural
+	  }
+
+let POSIX2001 =
+	  { SIGBUS :
+		  Natural
+	  , SIGPOLL :
+		  Natural
+	  , SIGPROF :
+		  Natural
+	  , SIGSYS :
+		  Natural
+	  , SIGTRAP :
+		  Natural
+	  , SIGURG :
+		  Natural
+	  , SIGVTALRM :
+		  Natural
+	  , SIGXCPU :
+		  Natural
+	  , SIGXFSZ :
+		  Natural
+	  }
+
+in  POSIX1990 â©“ POSIX2001
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/common/x86-arm.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/common/x86-arm.dhall
new file mode 100644
index 00000000..787afad9
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/common/x86-arm.dhall
@@ -0,0 +1,35 @@
+	./standard.dhall
+  ⫽ { SIGUSR1 =
+		10
+	, SIGUSR2 =
+		12
+	, SIGCHLD =
+		17
+	, SIGCONT =
+		18
+	, SIGSTOP =
+		19
+	, SIGTSTP =
+		20
+	, SIGTTIN =
+		21
+	, SIGTTOU =
+		22
+	, SIGBUS =
+		7
+	, SIGPOLL =
+		29
+	, SIGPROF =
+		27
+	, SIGSYS =
+		31
+	, SIGURG =
+		23
+	, SIGVTALRM =
+		26
+	, SIGXCPU =
+		24
+	, SIGXFSZ =
+		25
+	}
+: ./type.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/mips.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/mips.dhall
new file mode 100644
index 00000000..63e071ba
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/mips.dhall
@@ -0,0 +1 @@
+./common/mips.dhall ⫽ ./misc/mips.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/misc/alpha-sparc.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/alpha-sparc.dhall
new file mode 100644
index 00000000..2ce0b50d
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/alpha-sparc.dhall
@@ -0,0 +1,14 @@
+  { SIGIOT =
+	  6
+  , SIGEMT =
+	  7
+  , SIGIO =
+	  23
+  , SIGPWR =
+	  29
+  , SIGINFO =
+	  29
+  , SIGWINCH =
+	  28
+  }
+: ./common-type.dhall â©“ { SIGEMT : Natural, SIGINFO : Natural }
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/misc/common-type.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/common-type.dhall
new file mode 100644
index 00000000..4f4cec9a
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/common-type.dhall
@@ -0,0 +1 @@
+{ SIGIOT : Natural, SIGIO : Natural, SIGPWR : Natural, SIGWINCH : Natural }
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/misc/mips.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/mips.dhall
new file mode 100644
index 00000000..ac07c128
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/mips.dhall
@@ -0,0 +1,14 @@
+  { SIGIOT =
+	  6
+  , SIGEMT =
+	  7
+  , SIGIO =
+	  22
+  , SIGCLD =
+	  18
+  , SIGPWR =
+	  19
+  , SIGWINCH =
+	  20
+  }
+: ./common-type.dhall â©“ { SIGEMT : Natural, SIGCLD : Natural }
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/misc/x86-arm.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/x86-arm.dhall
new file mode 100644
index 00000000..c937a3a4
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/misc/x86-arm.dhall
@@ -0,0 +1,14 @@
+  { SIGIOT =
+	  6
+  , SIGSTKFLT =
+	  16
+  , SIGIO =
+	  29
+  , SIGPWR =
+	  30
+  , SIGWINCH =
+	  28
+  , SIGUNUSED =
+	  31
+  }
+: ./common-type.dhall â©“ { SIGSTKFLT : Natural, SIGUNUSED : Natural }
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/sparc.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/sparc.dhall
new file mode 100644
index 00000000..bdbad021
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/sparc.dhall
@@ -0,0 +1 @@
+./alpha.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/type.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/type.dhall
new file mode 100644
index 00000000..55f6540c
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/type.dhall
@@ -0,0 +1,2 @@
+-- A signal is a positive number
+Natural
\ No newline at end of file
diff --git a/pkgs/profpatsch/s6/dhall/unix/Signal/x86.dhall b/pkgs/profpatsch/s6/dhall/unix/Signal/x86.dhall
new file mode 100644
index 00000000..7030dd27
--- /dev/null
+++ b/pkgs/profpatsch/s6/dhall/unix/Signal/x86.dhall
@@ -0,0 +1 @@
+./common/x86-arm.dhall ⫽ ./misc/x86-arm.dhall
\ No newline at end of file
diff --git a/pkgs/profpatsch/sandbox.nix b/pkgs/profpatsch/sandbox.nix
new file mode 100644
index 00000000..733cd956
--- /dev/null
+++ b/pkgs/profpatsch/sandbox.nix
@@ -0,0 +1,63 @@
+{ pkgs, writeExecline }:
+
+let
+
+  # remove everything but a few selected environment variables
+  runInEmptyEnv = keepVars:
+    let
+        importas = pkgs.lib.concatMap (var: [ "importas" "-i" var var ]) keepVars;
+        # we have to explicitely call export here, because PATH is probably empty
+        export = pkgs.lib.concatMap (var: [ "${pkgs.execline}/bin/export" var ''''${${var}}'' ]) keepVars;
+    in writeExecline "empty-env" {}
+         (importas ++ [ "emptyenv" ] ++ export ++ [ "${pkgs.execline}/bin/exec" "$@" ]);
+
+
+  # lightweight sandbox; execute any command in an unshared
+  # namespace that only has access to /nix and the specified
+  # directories from `extraMounts`.
+  sandbox = { extraMounts ? [] }:
+    let
+      pathsToMount = [
+        "/nix"
+        "/dev" "/proc" "/sys"
+      ] ++ extraMounts;
+      # chain execlines and exit immediately if one fails
+      all = builtins.concatMap (c: [ "if" c ]);
+      mount = "${pkgs.utillinux}/bin/mount";
+      unshare = "${pkgs.utillinux}/bin/unshare";
+      # this is the directory the sandbox runs under (in a separate mount namespace)
+      newroot = pkgs.runCommandLocal "sandbox-root" {} ''mkdir "$out"'';
+      # this runs in a separate namespace, sets up a chroot root
+      # and then chroots into the new root.
+      sandbox = writeExecline "sandbox" {} (builtins.concatLists [
+        # first, unshare the mount namespace and make us root
+        # -> requires user namespaces!
+        [ unshare "--mount" "--map-root-user" ]
+        (all
+          # mount a temporary file system which we can chroot to;
+          # we can use the fixed path newroot here, because the resulting
+          # tmpfs cannot be seen from the outside world (we are in an unshared
+          # mount )
+          ([ [ mount "-t" "tmpfs" "container_root" newroot ] ]
+          # now mount the file system parts we need into the chroot
+          ++ builtins.concatMap
+               (rootPath: [
+                 [ "${pkgs.coreutils}/bin/mkdir" "-p" "${newroot}${rootPath}" ]
+                 [ mount "--rbind" rootPath "${newroot}${rootPath}" ]
+               ])
+               pathsToMount))
+        [ # finally, chroot into our new root directory
+          "${pkgs.coreutils}/bin/chroot" newroot
+          # drop root permissions, become user nobody;
+          # This is because many programs don’t like to be root
+          # TODO: this unshare does not work, because we don’t have
+          # the right permissions to do that here, unfortunately :(
+          # unshare "--user" "--"
+          "$@"
+        ]
+      ]);
+    in sandbox;
+
+in {
+  inherit sandbox runInEmptyEnv;
+}
diff --git a/pkgs/profpatsch/sfttime/default.nix b/pkgs/profpatsch/sfttime/default.nix
new file mode 100644
index 00000000..29d9170b
--- /dev/null
+++ b/pkgs/profpatsch/sfttime/default.nix
@@ -0,0 +1,14 @@
+{ stdenv, makeWrapper, bc }:
+
+stdenv.mkDerivation {
+  name = "sfttime";
+
+  phases = [ "installPhase" "fixupPhase" ];
+  buildInputs = [ makeWrapper ];
+
+  installPhase = ''
+    install -D ${./sfttime.sh} $out/bin/sfttime
+    wrapProgram $out/bin/sfttime \
+      --prefix PATH : ${stdenv.lib.makeBinPath [ bc ]}
+  '';
+}
diff --git a/pkgs/profpatsch/sfttime/sfttime.sh b/pkgs/profpatsch/sfttime/sfttime.sh
new file mode 100755
index 00000000..949341b7
--- /dev/null
+++ b/pkgs/profpatsch/sfttime/sfttime.sh
@@ -0,0 +1,145 @@
+#!/usr/bin/env bash
+# this script was created in 3BBE.81[sft].
+# usage:
+# to convert to sfttime:
+#	$0 [c date] [digitcount] [nodate]
+#	if date is given, converts the given date to sfttime.
+#	if date is not given, converts the current date to sfttime.
+#	digitcount specifies the accuracy for the time part.
+#	nodate hides the date part.
+# to convert from sfttime:
+#	$0 r sfttime [unix]
+#	converts the given sfttime to 'standard' time.
+#	if 'unix' is provided, the output will be in unix time.
+# to show info about sfttime units
+#	$0 i [[sft]]$num
+#	displays name of unit [sft]$num, as well as it's value
+#	in both days and 'standard' units.
+
+SFT_EPOCH_UNIX=49020
+
+case $1 in
+	"c")
+		unixtime=$(date --date="$2" +%s.%N)
+		shift
+		shift
+		mode=fw
+		;;
+	"r")
+		shift
+		sfttime=$1
+		if [[ $sfttime =~ ^([0-9A-F]*(.[0-9A-F]+)?)(\[[sS][fF][tT]\])?$ ]] && [[ $sfttime ]]; then
+			sfttime=${BASH_REMATCH[1]}
+		else
+			echo "error" 2>&1
+			exit 1
+		fi
+		shift
+		mode=bw
+		;;
+	"i")
+		shift
+		inforeq=$1
+		if [[ $inforeq =~ ^(\[[sS][fF][tT]\])?(-?[0-9]+)$ ]]; then
+			inforeq=${BASH_REMATCH[2]}
+			let inforeq=$inforeq
+			mode=in
+		elif [[ $inforeq =~ ^(\[[sS][fF][tT]\])?[eE][pP][oO][cC][hH]$ ]]; then
+			echo "[sft]epoch:"
+			echo "unix time $SFT_EPOCH_UNIX"
+			echo "1970-01-01 13:37:00 UTC"
+			exit 0
+		else
+			echo "error" 2>&1
+			exit 1
+		fi
+		shift
+		mode=in
+		;;
+	*)
+		unixtime=$(date +%s.%N)
+		mode=fw
+		;;
+esac
+
+case $mode in
+	"fw")
+		sfttime=$(echo "obase=16; ($unixtime-$SFT_EPOCH_UNIX)/86400" | bc -l)
+		if [[ $1 -ge 1 ]]; then
+			digits=$1
+			shift
+		elif [[ ! $1 ]] || [[ $1 == nodate ]]; then
+			digits=3
+		else
+			digits=0
+		fi
+
+		if [[ $sfttime =~ ^([0-9A-F]+)[.]([0-9A-F]{$digits}).*$ ]]; then
+			date=${BASH_REMATCH[1]}
+			time=${BASH_REMATCH[2]}
+		else
+			echo "Error" &1>2
+			exit 1
+		fi
+
+		if [[ $digits -eq 0 ]]; then
+			echo "$date[sft]"
+		else
+			if [[ $1 == nodate ]]; then
+				echo ".$time[sft]"
+				shift
+			else
+				echo "$date.$time[sft]"
+			fi
+		fi
+		;;
+	"bw")
+		unixtime=$(echo "ibase=16; $sfttime*15180+BF7C" | bc -l)
+		case $1 in
+			unix)
+				shift
+				echo $unixtime
+				;;
+			*)
+				date --date="1970-01-01 $unixtime sec"
+				;;
+		esac
+		;;
+	"in")
+		name="[sft]$inforeq"
+		case $inforeq in
+      -4) newname="[sft]tick";;
+			-3)	newname="[sft]tentacle";;
+			-2)	newname="[sft]schinken";;
+			-1)	newname="[sft]major";;
+			0)	newname="day";;
+			1)	newname="[sft]vergil";;
+			2)	newname="[sft]stallman";;
+			3)	newname="[sft]odin";;
+		esac
+		if [[ $newname ]]; then
+			echo "alternative name for $name: $newname"
+			name="$newname"
+		fi
+		one="1 $name"
+		echo "$one after [sft]epoch:"
+		sfttime=$(echo "obase=16; 16^$inforeq" | bc -l)[sft]
+		echo $sfttime
+		echo "time equivalent of $one:"
+		echo "the duration of $(echo 794243384928000*16^$inforeq | bc -l) periods of the radiation corresponding to the transition between the two hyperfine levels of the ground state of the caesium 133 atom"
+
+		echo "standard time units equivalent:"
+		seconds=$(echo "86400*16^$inforeq" | bc -l)
+		if [[ $(echo "$seconds < 60" | bc -l) == 1 ]]; then
+			echo "$seconds seconds"
+		elif [[ $(echo "$seconds < 3600" | bc -l) == 1 ]]; then
+			echo "$(echo $seconds/60 | bc -l) minutes"
+		elif [[ $(echo "$seconds < 86400" | bc -l) == 1 ]]; then
+			echo "$(echo $seconds/3600 | bc -l) hours"
+		elif [[ $(echo "$seconds < 86400*365.2425" | bc -l) == 1 ]]; then
+			echo "$(echo $seconds/86400 | bc -l) days"
+		else
+			echo "$(echo $seconds/86400/365.2425 | bc -l) years"
+		fi
+		;;
+esac
diff --git a/pkgs/profpatsch/show-qr-code/default.nix b/pkgs/profpatsch/show-qr-code/default.nix
new file mode 100644
index 00000000..17d9847a
--- /dev/null
+++ b/pkgs/profpatsch/show-qr-code/default.nix
@@ -0,0 +1,28 @@
+{ stdenv, writeScriptBin, gtkdialog, qrencode }:
+
+let script = writeScriptBin "show-qr-code" ''
+  #!/bin/sh
+  TMP=$(mktemp)
+  ${qrencode}/bin/qrencode -s 8 -o "$TMP" -t PNG "$1"
+
+  export DIALOG='
+  <vbox>
+      <pixmap>
+          <input file>'$TMP'</input>
+      </pixmap>
+  </vbox>
+  '
+
+  ${gtkdialog}/bin/gtkdialog --program=DIALOG > /dev/null &
+
+  sleep 0.2
+
+  rm "$TMP"
+
+  '';
+
+in script // {
+  meta = {
+    description = "Show the given string as qr code in a gtk window";
+  };
+}
diff --git a/pkgs/profpatsch/testing/default.nix b/pkgs/profpatsch/testing/default.nix
new file mode 100644
index 00000000..76d43ecc
--- /dev/null
+++ b/pkgs/profpatsch/testing/default.nix
@@ -0,0 +1,92 @@
+{ stdenv, runCommandLocal, lib
+, runExecline, bin }:
+
+let
+
+  /* Realize drvDep, then return drvOut if that succeds.
+   * This can be used to make drvOut depend on the
+   * build success of drvDep without making drvDep a
+   * dependency of drvOut
+   * => drvOut is not rebuilt if drvDep changes
+   */
+  drvSeq = drvDep: drvOut: drvSeqL [drvDep] drvOut;
+
+  /* TODO DOCS */
+  drvSeqL = drvDeps: drvOut: let
+  drvOutOutputs = drvOut.outputs or ["out"];
+  in
+    runCommandLocal drvOut.name {
+      # we inherit all attributes in order to replicate
+      # the original derivation as much as possible
+      outputs = drvOutOutputs;
+      passthru = drvOut.drvAttrs;
+      # depend on drvDeps (by putting it in builder context)
+      inherit drvDeps;
+    }
+    # the outputs of the original derivation are replicated
+    # by creating a symlink to the old output path
+    (lib.concatMapStrings (output: ''
+      target=${lib.escapeShellArg drvOut.${output}}
+      # if the target is already a symlink, follow it until it’s not;
+      # this is done to prevent too many dereferences
+      target=$(readlink -e "$target")
+      # link to the output
+      ln -s "$target" "${"$"}${output}"
+    '') drvOutOutputs);
+
+  /* Takes a derivation and an attribute set of
+   * test names to tests.
+   * Tests can be constructed by calling test
+   * functions like `bashTest` or `execlineTest`.
+   * They generally take scripts that
+   * is not sucessful and succeed otherwise.
+   */
+  withTests = tests: drv:
+    assert lib.isDerivation drv; # drv needs to be a derivation!
+    let testDrvs = lib.mapAttrsToList
+          (testName: testFun: testFun {
+            drvName = "${drv.name}-test-${testName}";
+          }) tests;
+    in drvSeqL testDrvs drv;
+
+  # /* Constructs a test from a bash script.
+  #  * The test will fail if the bash script exits
+  #  * with an exit code other than 0. */
+  # bashTest = testScript: { drvName }:
+  #   runCommand drvName {
+  #     preferLocalBuild = true;
+  #     allowSubstitutes = false;
+  #   } ''
+  #     ${testScript}
+  #     touch "$out"
+  #   '';
+
+  # /* Constructs a test from an execline script.
+  #  * The test will fail if the bash script exits
+  #  * with an exit code other than 0. */
+  # execlineTest = testScript: { drvName }:
+  #   runExecline {
+  #     name = drvName;
+  #     execline = testScript;
+  #     builderWrapper = runExecline {
+  #       name = "touch-out";
+  #       execline = ''
+  #         importas -i out out
+  #         ifte
+  #           # if $@ succeeds, $out is touched
+  #           { ${bin.s6-touch} $out }
+  #           # otherwise we return the exit code
+  #           { importas exit ?
+  #             ${bin.s6-echo} $exit }
+  #           # condition
+  #           $@
+  #       '';
+  #     derivationArgs = {
+  #       preferLocalBuild = true;
+  #       allowSubstitutes = false;
+  #     };
+  #   };
+  # };
+
+in
+  { inherit drvSeq drvSeqL withTests; }
diff --git a/pkgs/profpatsch/utils-hs/default.nix b/pkgs/profpatsch/utils-hs/default.nix
new file mode 100644
index 00000000..f54b0dfa
--- /dev/null
+++ b/pkgs/profpatsch/utils-hs/default.nix
@@ -0,0 +1,105 @@
+{ lib, fetchFromGitHub, haskellPackages, haskell }:
+
+let
+  utilsSrc = fetchFromGitHub {
+    owner = "Profpatsch";
+    repo = "utils.hs";
+    rev = "8a085c306a864561bc78828c965f94c5db3fc31a";
+    sha256 = "1fpsfylk5vz8qg4fyjg8zniwnsj5fvnsazpspi76jn24541ffc2y";
+  };
+  version = "git";
+
+  # TODO: make it possible to override the hps fixpoint again
+  # without removing the overrides in here
+  hps =
+    let hlib = haskell.lib; in
+    haskellPackages.override {
+      overrides = (hself: hsuper: {
+
+        # shell stub
+        shellFor = f: # self -> { buildDepends, buildTools }
+          let args = f hself;
+          in hsuper.mkDerivation {
+            pname = "pkg-env";
+            src = "/dev/null";
+            version = "none";
+            license = "none";
+            inherit (args) buildDepends;
+            buildTools = with hself; [
+              ghcid
+              cabal-install
+              hpack
+              (hoogleLocal {
+                packages = args.buildDepends;
+              })
+            ] ++ args.buildTools or [];
+          };
+
+        # hoogleLocal should never use the builders
+        hoogleLocal = args: (hsuper.hoogleLocal args).overrideAttrs (_: {
+          preferLocalBuild = true;
+          allowSubstitutes = false;
+        });
+
+        these = hlib.doJailbreak hsuper.these;
+
+        hnix = hlib.overrideCabal
+          (hsuper.hnix.override {
+            inherit (hself) these;
+          }) (old: {
+          src = fetchFromGitHub {
+            owner = "haskell-nix";
+            repo = "hnix";
+            rev = "e7efbb4f0624e86109acd818942c8cd18a7d9d3d";
+            sha256 = "0dismb9vl5fxynasc2kv5baqyzp6gpyybmd5p9g1hlcq3p7pfi24";
+          };
+          broken = false;
+          buildDepends = old.buildDepends or [] ++ (with hself; [
+            dependent-sum prettyprinter (hlib.doJailbreak ref-tf)
+          ]);
+        });
+      });
+    };
+
+  haskellDrv = { name, subfolder, deps }: hps.mkDerivation {
+    pname = name;
+    inherit version;
+    src = "${utilsSrc}/${subfolder}";
+    # TODO make utils.hs buildable from the project itself
+    # src = "${/home/philip/code/haskell/utils.hs}/${subfolder}";
+    license = lib.licenses.gpl3;
+    isExecutable = true;
+    hydraPlatforms = [ "x86_64-linux" ];
+    buildDepends = deps;
+
+    # justStaticExecutables
+    enableSharedExecutables = false;
+    enableLibraryProfiling = false;
+    isLibrary = false;
+    doHaddock = false;
+    postFixup = "rm -rf $out/lib $out/nix-support $out/share/doc";
+  };
+
+
+  nix-gen = haskellDrv {
+    name = "nix-gen";
+    subfolder = "nix-gen";
+    deps = with hps; [ hnix ansi-wl-pprint protolude data-fix ];
+  };
+
+  until = haskellDrv {
+    name = "until";
+    subfolder = "until";
+    deps = with hps; [ optparse-applicative data-fix time];
+  };
+
+  watch-server = haskellDrv {
+    name = "watch-server";
+    subfolder = "watch-server";
+    deps = with hps; [ directory protolude fsnotify regex-tdfa optparse-generic ];
+  };
+
+in {
+  inherit nix-gen until watch-server;
+  haskellPackages = hps;
+}
diff --git a/pkgs/profpatsch/warpspeed/default.nix b/pkgs/profpatsch/warpspeed/default.nix
new file mode 100644
index 00000000..911969fb
--- /dev/null
+++ b/pkgs/profpatsch/warpspeed/default.nix
@@ -0,0 +1,60 @@
+{ lib, runCommand, ghcWithPackages }:
+
+let
+  name = "warpspeed-1.1";
+
+  script = builtins.toFile "${name}.hs" ''
+    {-# LANGUAGE OverloadedStrings #-}
+    module Main where
+
+    import Safe
+    import Data.String (fromString)
+    import Data.List (intercalate)
+    import System.Environment (getArgs)
+    import System.Exit (die)
+    import Network.Wai
+    import Network.Wai.Middleware.Static
+    import Network.Wai.Handler.Warp
+    import Network.HTTP.Types.Status
+    import qualified Debug.Trace
+
+    usage :: IO ()
+    usage = die $ intercalate "\n"
+      [ "usage: warpspeed <host> <port> [root-redirect]"
+      , ""
+      , "<host>: `*6` means any host, IPv6 preferred."
+      , "See https://hackage.haskell.org/package/warp-3.3.5/docs/Network-Wai-Handler-Warp.html#t:HostPreference for the host binding syntax."
+      ]
+
+    rootRedirectPolicy :: String -> Policy
+    rootRedirectPolicy redirTo = policy (\s -> Just $ if (Debug.Trace.traceShowId s) == "" then redirTo else s)
+
+    main :: IO ()
+    main = do
+      args <- getArgs
+      let portOrUsage port act = maybe usage act (readMay port :: Maybe Int)
+      case args of
+        [] -> usage
+        [_] -> usage
+        [ host, port ] -> portOrUsage port $ \p -> serve host p Nothing
+        [ host, port, redirectTo ] -> portOrUsage port $ \p -> serve host p (Just redirectTo)
+        _ -> usage
+      where
+        settings host port =
+            setPort port
+          $ setHost (fromString host)
+          $ defaultSettings
+        serve host port redirectTo =
+            runSettings (settings host port)
+          $ staticPolicy (maybe mempty rootRedirectPolicy redirectTo)
+          $ \_ resp -> resp $ responseLBS notFound404 [] ""
+   '';
+
+   deps = hp: with hp; [ wai-middleware-static warp safe ];
+
+in runCommand name {
+  meta.description = "Trivial and very fast static HTTP file server";
+} ''
+  mkdir -p $out/bin
+  ${ghcWithPackages deps}/bin/ghc -O2 -Wall -o "$out/bin/warpspeed" ${script}
+''
diff --git a/pkgs/profpatsch/xmonad/DhallTypedInput.hs b/pkgs/profpatsch/xmonad/DhallTypedInput.hs
new file mode 100644
index 00000000..18c32b22
--- /dev/null
+++ b/pkgs/profpatsch/xmonad/DhallTypedInput.hs
@@ -0,0 +1,232 @@
+{-# language RecordWildCards, NamedFieldPuns, OverloadedStrings, ScopedTypeVariables, KindSignatures, DataKinds, ScopedTypeVariables, RankNTypes, GADTs, TypeApplications, AllowAmbiguousTypes, LambdaCase #-}
+{- Exports the `inputWithTypeArgs` function, which is able to read dhall files of the normalized form
+
+@
+\(CustomType: Type) ->
+\(AnotherType: Type) ->
+…
+@
+
+and set their actual representation on the Haskell side:
+
+This has various advantages:
+
+- dhall files still type check & normalize with the normal dhall
+  tooling, they are standalone (and the types can be instantiated from
+  dhall as well without any workarounds)
+- It can be used like the default `input` function, no injection of
+  custom symbols in the Normalizer is reqired
+- Brings this style of dhall integration to Haskell, where it was only
+  feasible in nix before, because that is untyped
+
+The dhall types can be instantiated by every Haskell type that has an
+`Interpret` instance. The “name†of the type lambda variable is
+compared on the Haskell side with a type-level string that the user
+provides, to prevent mixups.
+
+TODO:
+- Improve error messages (!)
+- Provide a way to re-use the type mapping on the Haskell side, so
+  that the returned values are not just the normal `Interpret` types,
+  but the mapped ones (with name phantom type)
+-}
+module DhallTypedInput
+( inputWithTypeArgs, TypeArg(..), TypeArgError(..), TypeArgEx(..),  typeArg
+)
+where
+
+import Control.Monad.Trans.State.Strict as State
+import Data.List (foldl')
+import Control.Exception (Exception)
+import qualified Control.Exception
+import qualified Data.Text as Text
+
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+import Data.Proxy (Proxy(Proxy))
+
+import Dhall (Type(..), InvalidType(..), InputSettings(..), EvaluateSettings(..), rootDirectory, startingContext, normalizer, standardVersion, sourceName, defaultEvaluateSettings, Interpret(..), auto)
+import Dhall.TypeCheck (X)
+import Dhall.Core
+import Dhall.Parser (Src(..))
+import qualified Dhall.Import
+import qualified Dhall.Pretty
+import qualified Dhall.TypeCheck
+import qualified Dhall.Parser
+
+import Lens.Family (LensLike', set, view)
+
+import Data.Text.Prettyprint.Doc (Pretty)
+import qualified Data.Text.Prettyprint.Doc               as Pretty
+import qualified Data.Text.Prettyprint.Doc.Render.Text   as Pretty
+import qualified Data.Text.Prettyprint.Doc.Render.String as Pretty
+
+
+-- | Information about a type argument in the dhall input
+--
+-- If the dhall file starts with @\(CustomType : Type) ->@,
+-- that translates to @TypeArg "CustomType" interpretionType@
+-- where @"CustomType"@ is a type-level string describing the
+-- name of the type in the dhall file (as a sanity check) and
+-- @interpretationType@ is any type which implements
+-- 'Dhall.Interpret'.
+--
+-- This is basically a specialized 'Data.Proxy'.
+data TypeArg (sym :: Symbol) t = TypeArg
+
+-- | Existential wrapper of a 'TypeArg', allows to create a list
+-- of heterogenous 'TypeArg's.
+data TypeArgEx
+  where TypeArgEx :: (KnownSymbol sym, Interpret t) => TypeArg sym t -> TypeArgEx
+
+-- | Shortcut for creating a 'TypeArgEx'.
+--
+-- Use with @TypeApplications@:
+--
+-- @
+-- typeArg @"CustomType" @Integer
+-- @
+typeArg :: forall sym t. (KnownSymbol sym, Interpret t) => TypeArgEx
+typeArg = TypeArgEx (TypeArg :: TypeArg sym t)
+
+-- | Possible errors returned when applying a 'TypeArg'
+-- to a 'Dhall.Expr'.
+data TypeArgError
+  = WrongLabel Text.Text
+  -- ^ The name (label) of the type was different,
+  -- the text value is the expected label.
+  | NoLambda
+  -- ^ The 'Dhall.Expr' does not start with 'Dhall.Lam'.
+
+-- | Apply a 'TypeArg' to a 'Dhall.Expr'.
+--
+-- Checks that the dhall file starts with the 'Dhall.Lam'
+-- corresponding to 'TypeArg`, then applies @t@ (dhall type application)
+-- and normalizes, effectively stripping the 'Dhall.Lam'.
+applyTypeArg
+  :: forall sym t. (KnownSymbol sym, Interpret t)
+  => Expr Src X
+  -> TypeArg sym t
+  -> Either TypeArgError (Expr Src X)
+applyTypeArg expr ta@(TypeArg) = case expr of
+  (Lam label (Const Dhall.Core.Type) _)
+    -> let expectedLabel = getLabel ta
+        in if label /= getLabel ta
+           then Left (WrongLabel expectedLabel)
+           else let expr' = (normalize (App expr tExpect))
+                  in Right expr'
+    where
+        Dhall.Type _ tExpect = Dhall.auto :: Dhall.Type t
+  expr -> Left NoLambda
+
+-- | Inflect the type-level string @sym@ to a text value.
+getLabel :: forall sym t. (KnownSymbol sym) => TypeArg sym t -> Text.Text
+getLabel _ = Text.pack $ symbolVal (Proxy :: (Proxy :: Symbol -> *) sym)
+
+instance (KnownSymbol sym) => Show (TypeArg sym t) where
+  show TypeArg =
+    "TypeArg "
+    ++ (symbolVal (Proxy :: (Proxy :: Symbol -> *) sym))
+
+-- | Takes a list of 'TypeArg's and parses the given
+-- dhall string, applying the given 'TypeArg's in order
+-- to the opaque dhall type arguments (see 'TypeArg' for
+-- how these should look).
+--
+-- This is a slightly changed 'Dhall.inputWith'.
+--
+-- Discussion: Any trace of our custom type is removed from
+-- the resulting
+inputWithTypeArgs
+  :: InputSettings
+  -> [TypeArgEx]
+  -> Dhall.Type a
+  -> Text.Text
+  -> IO a
+inputWithTypeArgs settings typeArgs (Dhall.Type {extract, expected}) txt = do
+    expr <- throws (Dhall.Parser.exprFromText (view sourceName settings) txt)
+
+    -- TODO: evaluateSettings not exposed
+    -- let evSettings = view evaluateSettings settings
+    let evSettings :: EvaluateSettings = defaultEvaluateSettings
+
+    -- -vvv copied verbatim from 'Dhall.inputWith' vvv-
+    let transform =
+               set Dhall.Import.standardVersion
+               (view standardVersion evSettings)
+            .  set Dhall.Import.normalizer
+               (view normalizer evSettings)
+            .  set Dhall.Import.startingContext
+               (view startingContext evSettings)
+
+    let status = transform (Dhall.Import.emptyStatus
+                            (view rootDirectory settings))
+
+    expr' <- State.evalStateT (Dhall.Import.loadWith expr) status
+    -- -^^^ copied verbatim ^^^-
+
+    let
+      -- | if there’s a note, run the transformation and rewrap with the note
+      skipNote e f = case e of
+          Note n e -> Note n $ f e
+          e -> f e
+
+    let
+      -- | strip one 'TypeArg'
+      stripTypeArg :: Expr Src X -> TypeArgEx -> Expr Src X
+      stripTypeArg e (TypeArgEx ta) = skipNote e $ \e' -> case e' of
+          (Lam label _ _) ->
+            case applyTypeArg e' ta of
+              Right e'' -> e''
+              -- TODO obvously improve error messages
+              Left (WrongLabel l) ->
+                error $ "Wrong label, should have been `" ++ Text.unpack l ++ "` but was `" ++ Text.unpack label ++ "`"
+              Left NoLambda -> error $ "I expected a lambda of the form λ(" ++ Text.unpack label ++ ": Type) → but got: " ++ show e
+          e' -> error $ show e'
+
+    -- strip all 'TypeArg's
+    let expr'' = foldl' stripTypeArg expr' typeArgs
+
+    -- -vvv copied verbatim as well (expr' -> expr'') vvv-
+    let suffix = prettyToStrictText expected
+    let annot = case expr'' of
+            Note (Src begin end bytes) _ ->
+                Note (Src begin end bytes') (Annot expr'' expected)
+              where
+                bytes' = bytes <> " : " <> suffix
+            _ ->
+                Annot expr'' expected
+
+    _ <- throws (Dhall.TypeCheck.typeWith (view startingContext settings) annot)
+    case extract (Dhall.Core.normalizeWith (Dhall.Core.getReifiedNormalizer (view normalizer settings)) expr'') of
+        Just x  -> return x
+        Nothing -> Control.Exception.throwIO InvalidType
+
+
+-- copied from Dhall.Pretty.Internal
+prettyToStrictText :: Pretty a => a -> Text.Text
+prettyToStrictText = docToStrictText . Pretty.pretty
+
+-- copied from Dhall.Pretty.Internal
+docToStrictText :: Pretty.Doc ann -> Text.Text
+docToStrictText = Pretty.renderStrict . Pretty.layoutPretty options
+  where
+   options = Pretty.LayoutOptions { Pretty.layoutPageWidth = Pretty.Unbounded }
+
+-- copied from somewhere in Dhall
+throws :: Exception e => Either e a -> IO a
+throws (Left  e) = Control.Exception.throwIO e
+throws (Right r) = return r
+
+
+-- TODO: add errors like these
+-- data WrongTypeLabel = WrongTypeLabel deriving (Typeable)
+
+-- _ERROR :: String
+-- _ERROR = "\ESC[1;31mError\ESC[0m"
+
+-- instance Show WrongTypeLabel where
+--     show WrongTypeLabel =
+--         _ERROR <> ": Mislabelled type lambda
+--         \                                                                                \n\
+--         \Expected your t provide an extract function that succeeds if an expression      \n\
+--         \matches the expected type.  You provided a Type that disobeys this contract     \n"
diff --git a/pkgs/profpatsch/youtube2audiopodcast/Main.hs b/pkgs/profpatsch/youtube2audiopodcast/Main.hs
new file mode 100755
index 00000000..4d0f9a93
--- /dev/null
+++ b/pkgs/profpatsch/youtube2audiopodcast/Main.hs
@@ -0,0 +1,113 @@
+{-# language OverloadedStrings #-}
+{-# language RecordWildCards #-}
+{-# language NamedFieldPuns #-}
+{-# language DeriveGeneric #-}
+module Main where
+
+import Data.Text (Text)
+import Data.Text.Lazy.IO as TIO
+import Text.RSS.Syntax
+import Text.Feed.Types (Feed(RSSFeed))
+import Text.Feed.Export (textFeed)
+
+import qualified Data.Aeson as Json
+import Data.Aeson (FromJSON)
+import GHC.Generics (Generic)
+import qualified Data.ByteString.Lazy as BS
+
+-- | Info from the config file
+data Config = Config
+  { channelName :: Text
+  , channelURL :: Text
+  }
+  deriving (Show, Generic)
+instance FromJSON Config where
+
+data Channel = Channel
+  { channelInfo :: ChannelInfo
+  , channelItems :: [ItemInfo]
+  }
+  deriving (Show, Generic)
+instance FromJSON Channel where
+
+-- | Info fetched from the channel
+data ChannelInfo = ChannelInfo
+
+  { channelDescription :: Text
+  , channelLastUpdate :: DateString -- TODO
+  , channelImage :: Maybe () --RSSImage
+  }
+  deriving (Show, Generic)
+instance FromJSON ChannelInfo where
+
+-- | Info of each channel item
+data ItemInfo = ItemInfo
+  { itemTitle :: Text
+  , itemDescription :: Text
+  , itemYoutubeLink :: Text
+  , itemCategory :: Text
+  , itemTags :: [Text]
+  , itemURL :: Text
+  , itemSizeBytes :: Integer
+  , itemHash :: Text
+  }
+  deriving (Show, Generic)
+instance FromJSON ItemInfo where
+
+-- main = print $ textFeed $ RSSFeed $ toRSS
+--   (ChannelInfo
+--     { channelDescription = "description"
+--     , channelLastUpdate = "some date"
+--     , channelImage = nullImage "imageURL" "imageTitle" "imageLink"
+--     })
+--   []
+main :: IO ()
+main = do
+  input <- BS.getContents
+  let (Right rss) = Json.eitherDecode input
+  let exConfig = (Config
+        { channelName = "channel name"
+        , channelURL = "channel url"
+        })
+  let (Just feed) = textFeed $ RSSFeed $ toRSS exConfig rss
+  TIO.putStr feed
+
+toRSS :: Config -> Channel -> RSS
+toRSS Config{..} Channel{channelInfo, channelItems}  =
+  let ChannelInfo{..} = channelInfo in
+  (nullRSS channelName channelURL)
+  { rssChannel = (nullChannel channelName channelURL)
+      { rssDescription = channelDescription
+      , rssLastUpdate = Just channelLastUpdate
+      , rssImage = Nothing --channelImage TODO
+      , rssGenerator = Just "youtube2audiopodcast"
+      , rssItems = map rssItem channelItems
+      }
+  }
+
+  where
+    rssItem ItemInfo{..} = (nullItem itemTitle)
+      { rssItemLink = Just itemYoutubeLink
+      , rssItemDescription = Just itemDescription
+      -- rssItemAuthor =
+      , rssItemCategories =
+        map (\name -> RSSCategory
+              { rssCategoryDomain = Nothing
+              , rssCategoryAttrs = []
+              , rssCategoryValue = name
+              }) $ itemCategory : itemTags
+      , rssItemEnclosure = Just $ RSSEnclosure
+          { rssEnclosureURL = itemURL
+          , rssEnclosureLength = Just itemSizeBytes
+          , rssEnclosureType = "audio/opus"
+          , rssEnclosureAttrs = []
+          }
+      , rssItemGuid = Just $ RSSGuid
+          { rssGuidPermanentURL = Just False
+          , rssGuidAttrs = []
+          , rssGuidValue = itemHash
+          }
+      -- TODO: date conversion
+      -- https://tools.ietf.org/html/rfc822#section-5.1
+      -- , rssItemPubDate 
+      }
diff --git a/pkgs/profpatsch/youtube2audiopodcast/default.nix b/pkgs/profpatsch/youtube2audiopodcast/default.nix
new file mode 100644
index 00000000..4cc6e6be
--- /dev/null
+++ b/pkgs/profpatsch/youtube2audiopodcast/default.nix
@@ -0,0 +1,173 @@
+{ pkgs, lib, writeExecline, writeHaskellInterpret, getBins, runInEmptyEnv, sandbox }:
+
+config@{ url, internalPort }:
+
+let
+  bins = getBins pkgs.hello [ "hello" ]
+    // getBins pkgs.coreutils [ "printf" "wc" "tr" "cut" "mktemp" "mkdir" "ls" ]
+    // getBins pkgs.youtube-dl [ "youtube-dl" ]
+    // getBins pkgs.s6-networking [ "s6-tcpserver" ]
+    // getBins pkgs.execline [ "fdmove" "backtick" "importas" "if" "redirfd" "pipeline" ]
+    // getBins pkgs.s6-portable-utils [
+         { use = "s6-cat"; as = "cat"; }
+       ]
+    // getBins pkgs.jl [ "jl" ];
+
+  # fetch the audio of a youtube video to ./audio.opus, given video ID
+  youtube-dl-audio = writeExecline "youtube-dl-audio" { readNArgs = 1; } [
+    bins.youtube-dl
+      "--verbose"
+      "--extract-audio"
+      "--audio-format" "opus"
+      # We have to give a specific filename (with the right extension).
+      # youtube-dl is really finicky with output filenames.
+      "--output" "./audio.opus"
+      "https://www.youtube.com/watch?v=\${1}"
+  ];
+
+  # print youtube playlist information to stdout, given playlist ID
+  youtube-playlist-info = writeExecline "youtube-playlist-info" { readNArgs = 1; } [
+    bins.youtube-dl
+      "--verbose"
+      # don’t query detailed info of every video,
+      # which takes a lot of time
+      "--flat-playlist"
+      # print a single line of json to stdout
+      "--dump-single-json"
+      "--yes-playlist"
+      "https://www.youtube.com/playlist?list=\${1}"
+  ];
+
+  printFeed = writeHaskellInterpret "print-feed" {
+    withPackages = hps: [ hps.feed hps.aeson ];
+  } ./Main.hs;
+
+  # minimal CGI request parser for use as UCSPI middleware
+  yolo-cgi = pkgs.writers.writePython3 "yolo-cgi" {} ''
+    import sys
+    import os
+
+
+    def parse_ass(bool):
+        if not bool:
+            sys.exit(1)
+
+
+    inbuf = sys.stdin.buffer
+
+    first_line = inbuf.readline().rstrip(b"\n").split(sep=b" ")
+    parse_ass(len(first_line) == 3)
+    parse_ass(first_line[2].startswith(b"HTTP/"))
+
+    os.environb[b"REQUEST_METHOD"] = first_line[0]
+    os.environb[b"REQUEST_URI"] = first_line[1]
+
+    cmd = sys.argv[1]
+    args = sys.argv[2:] if len(sys.argv) > 2 else []
+    os.execlp(cmd, cmd, *args)
+  '';
+
+  # print the contents of an envar to the stdin of $@
+  envvar-to-stdin = writeExecline "envvar-to-stdin" { readNArgs = 1; } [
+    "importas" "VAR" "$1"
+    "pipeline" [ bins.printf "%s" "$VAR" ] "$@"
+  ];
+
+  # serve an opus file as HTTP on stdout
+  serve-http-file = content-type:
+    writeExecline "serve-http-file" { readNArgs = 1; } [
+      # determine file size
+      bins.backtick "-i" "-n" "filesize" [
+        bins.redirfd "-r" "0" "$1"
+        bins.wc "--bytes"
+      ]
+      bins.importas "filesize" "filesize"
+      # yolo html
+      bins.${"if"} [ bins.printf ''
+        HTTP/1.1 200 OK
+        Content-Type: ${content-type}
+        Content-Length: %u
+
+      '' "$filesize" ]
+      # the payload is our file
+      bins.redirfd "-r" "0" "$1" bins.cat
+    ];
+
+  dispatch-request = pkgs.writers.writeDash "dispatch-request" ''
+    case "$REQUEST_URI" in
+      /playlist/*)
+        ${bins.mkdir} /tmp >&2
+        ${bins.mkdir} /work >&2
+        ${print-feed-xml} "''${REQUEST_URI#/playlist/}" >/work/feed \
+          && ${serve-http-file "text/xml"} /work/feed
+        ;;
+      /video/*)
+        ${youtube-dl-audio} "''${REQUEST_URI#/video/}" 1>&2 \
+          && ${serve-http-file "audio/ogg"} "./audio.opus"
+        ;;
+      *) return 1 ;;
+    esac
+  '';
+
+  http-server = writeExecline "http-server" {} [
+    (runInEmptyEnv [])
+    bins.s6-tcpserver "127.0.0.1" config.internalPort
+    (sandbox { extraMounts = [ "/etc" ]; })
+    yolo-cgi
+    dispatch-request
+  ];
+
+  example-config = pkgs.writeText "example-config.json" (lib.generators.toJSON {} {
+    channelName = "Lonely Rolling Star";
+    channelURL = "https://www.youtube.com/playlist?list=PLV9hywkogVcOuHJ8O121ulSfFDKUhJw66";
+  });
+
+  transform-flat-playlist-to-rss = { videoUrl }:
+    let
+      playlist-item-info-jl = ''
+        (\o ->
+          { itemTitle: o.title
+          , itemYoutubeLink: append "https://youtube.com/watch?v=" o.id
+          ${/*TODO how to add the url here nicely?*/""}
+          , itemURL: append "${videoUrl}/" o.id
+
+          ${/*# TODO*/""}
+          , itemDescription: ""
+          , itemCategory: ""
+          , itemTags: []
+          , itemSizeBytes: 0
+          , itemHash: ""
+          })
+     '';
+     playlist-info-jl = ''
+       \pl ->
+         { channelInfo:
+           { channelDescription: pl.title
+
+           ${/*# TODO*/""}
+           , channelLastUpdate: "000"
+           , channelImage: null
+           }
+         , channelItems:
+           map
+             ${playlist-item-info-jl}
+             pl.entries
+         }
+     '';
+   in writeExecline "youtube-dl-playlist-json-to-rss-json" {} [
+    bins.jl playlist-info-jl
+  ];
+
+  ex = "PLV9hywkogVcOuHJ8O121ulSfFDKUhJw66";
+
+  print-feed-xml = writeExecline "print-feed-xml" { readNArgs = 1; } [
+    "pipeline" [
+      youtube-playlist-info "$1"
+    ]
+    "pipeline" [
+      (transform-flat-playlist-to-rss { videoUrl = "${config.url}/video"; })
+    ]
+    printFeed
+  ];
+
+in http-server
diff --git a/pkgs/sternenseemann/default.nix b/pkgs/sternenseemann/default.nix
new file mode 100644
index 00000000..d3b29076
--- /dev/null
+++ b/pkgs/sternenseemann/default.nix
@@ -0,0 +1,6 @@
+{ haskellPackages, ocamlPackages }:
+
+{
+  spacecookie = haskellPackages.callPackage ./spacecookie {};
+  logbook = ocamlPackages.callPackage ./logbook {};
+}
diff --git a/pkgs/sternenseemann/logbook/default.nix b/pkgs/sternenseemann/logbook/default.nix
new file mode 100644
index 00000000..78f12f4d
--- /dev/null
+++ b/pkgs/sternenseemann/logbook/default.nix
@@ -0,0 +1,26 @@
+{ stdenv, ocaml, topkg, ocamlbuild, findlib, ocaml_lwt
+, jingoo, ptime, angstrom, astring, opam, cow
+, fetchgit }:
+
+stdenv.mkDerivation rec {
+  version = "2017-02-18";
+  name = "ocaml${ocaml.version}-logbook-${version}";
+
+  src = fetchgit {
+    url    = "https://github.com/sternenseemann/logbook";
+    rev    = "1834ced22e4faf1e3afb3519febc176209099526";
+    sha256 = "1jq43n28s5k59hnl5xawzqvgmnknccanyvf6s8zwyfw3m60qsnd2";
+  };
+
+  buildInputs = [ ocaml findlib ocamlbuild topkg opam cow
+                  ocaml_lwt jingoo ptime angstrom astring
+                ];
+
+  inherit (topkg) buildPhase installPhase;
+  meta = with stdenv.lib; {
+    description = "A tool for personal log files";
+    platforms = ocaml.meta.platforms;
+    hydraPlatforms = [ "x86_64-linux" ];
+    license = licenses.bsd3;
+  };
+}
diff --git a/pkgs/sternenseemann/spacecookie/default.nix b/pkgs/sternenseemann/spacecookie/default.nix
new file mode 100644
index 00000000..42ee21fa
--- /dev/null
+++ b/pkgs/sternenseemann/spacecookie/default.nix
@@ -0,0 +1,25 @@
+{ mkDerivation, aeson, attoparsec, base, bytestring, containers
+, directory, fetchgit, filepath, hxt-unicode, mtl, socket, stdenv
+, transformers, unix, fast-logger
+}:
+mkDerivation {
+  pname = "spacecookie";
+  version = "0.2.0.0";
+  src = fetchgit {
+    url = "https://github.com/sternenseemann/spacecookie";
+    sha256 = "1hsanzhxg29alc49rlfny778afn6xznjamnqd8m7a4ynj3iswg42";
+    rev = "39001d0f70891caab774376a48f61b91a66d9f30";
+  };
+  isLibrary = true;
+  isExecutable = true;
+  libraryHaskellDepends = [
+    attoparsec base bytestring containers directory filepath
+    hxt-unicode mtl socket transformers unix fast-logger
+  ];
+  executableHaskellDepends = [
+    aeson attoparsec base bytestring containers directory filepath mtl
+    transformers unix
+  ];
+  description = "gopher server daemon";
+  license = stdenv.lib.licenses.gpl3;
+}
diff --git a/pkgs/taalo-build/default.nix b/pkgs/taalo-build/default.nix
new file mode 100644
index 00000000..1c24b316
--- /dev/null
+++ b/pkgs/taalo-build/default.nix
@@ -0,0 +1,36 @@
+{ stdenv, lib, runCommandLocal, coreutils, nix }:
+
+let
+  mkNixRemote = proto: let
+    hostAndQuery = "nix-remote-build@taalo.headcounter.org?compress=true";
+  in "${proto}://${hostAndQuery}";
+
+  remoteCopyEsc = lib.escapeShellArg (mkNixRemote "ssh");
+  remoteEsc = lib.escapeShellArg (mkNixRemote "ssh-ng");
+  mkNix = cmd: lib.escapeShellArg "${nix}/bin/${cmd}";
+
+  errorOnly = cmd:
+    "if ! outerr=\"$(${cmd} 2>&1)\"; then echo \"$outerr\" >&2; exit 1; fi";
+
+  remoteRealize = pre: arg: ''
+    ${errorOnly "${mkNix "nix"} copy -s --quiet --to ${remoteCopyEsc} ${arg}"}
+    NIX_REMOTE=${remoteEsc} ${pre}${mkNix "nix-store"} -r ${arg}
+  '';
+
+  emitScript = content: let
+    result = "#!${stdenv.shell}\nset -e\n${content}";
+  in "echo -n ${lib.escapeShellArg result}";
+
+in runCommandLocal "taalo-build" {} ''
+  mkdir -p "$out/bin"
+
+  ${emitScript (''
+    gctmp="$(${lib.escapeShellArg "${coreutils}/bin/mktemp"} -d)"
+    trap 'rm -rf "$gctmp"' EXIT
+    drv="$(${mkNix "nix-instantiate"} --add-root "$gctmp/drv" --indirect "$@")"
+  '' + remoteRealize "" "$drv")} > "$out/bin/taalo-build"
+
+  ${emitScript (remoteRealize "exec " "\"$@\"")} > "$out/bin/taalo-realize"
+
+  chmod +x "$out"/bin/taalo-{build,realize}
+''
diff --git a/release.nix b/release.nix
new file mode 100644
index 00000000..21fc1e6e
--- /dev/null
+++ b/release.nix
@@ -0,0 +1,229 @@
+{ vuizvuiSrc ? null
+, nixpkgsSrc ? <nixpkgs>
+, supportedSystems ? [ "i686-linux" "x86_64-linux" ]
+}:
+
+let
+  nixpkgsRevCount = nixpkgsSrc.revCount or 12345;
+  nixpkgsShortRev = nixpkgsSrc.shortRev or "abcdefg";
+  nixpkgsVersion = "pre${toString nixpkgsRevCount}.${nixpkgsShortRev}-vuizvui";
+
+  nixpkgs = nixpkgsSrc;
+
+  vuizvuiRevCount = vuizvuiSrc.revCount or 12345;
+  vuizvuiShortRev = vuizvuiSrc.shortRev or "abcdefg";
+  vuizvuiVersion = "pre${toString vuizvuiRevCount}.${vuizvuiShortRev}";
+
+  vuizvui = let
+    patchedVuizvui = (import nixpkgs {}).stdenv.mkDerivation {
+      name = "vuizvui-${vuizvuiVersion}";
+      inherit nixpkgsVersion;
+      src = vuizvuiSrc;
+      phases = [ "unpackPhase" "installPhase" ];
+      installPhase = ''
+        cp -r --no-preserve=ownership "${nixpkgs}/" nixpkgs
+        chmod -R u+w nixpkgs
+        echo -n "$nixpkgsVersion" > nixpkgs/.version-suffix
+        echo "echo '$nixpkgsVersion'" \
+          > nixpkgs/nixos/modules/installer/tools/get-version-suffix
+        echo -n ${nixpkgs.rev or nixpkgsShortRev} > nixpkgs/.git-revision
+        echo './nixpkgs' > nixpkgs-path.nix
+        cp -r . "$out"
+      '';
+    };
+  in if vuizvuiSrc == null then ./. else patchedVuizvui;
+
+  system = "x86_64-linux";
+  pkgsUpstream = import nixpkgs { inherit system; };
+  root = import vuizvui { inherit system; };
+
+  mpath = if vuizvuiSrc == null then ./machines else "${vuizvui}/machines";
+
+  allMachines = with pkgsUpstream.lib; let
+    wrapPkgs = machine: machine.__withPkgsPath nixpkgs;
+    condition = m: !(m ? __withPkgsPath);
+  in mapAttrsRecursiveCond condition (const wrapPkgs) (import mpath);
+
+  allTests = with import ./lib; getVuizvuiTests ({
+    inherit system nixpkgs;
+    excludeVuizvuiGames = true;
+  } // pkgsUpstream.lib.optionalAttrs (vuizvuiSrc != null) {
+    vuizvuiTests = "${vuizvui}/tests";
+  });
+
+  pkgs = with pkgsUpstream.lib; let
+    noGames = flip removeAttrs [ "games" ];
+    releaseLib = import "${nixpkgs}/pkgs/top-level/release-lib.nix" {
+      inherit supportedSystems;
+      packageSet = attrs: noGames (import vuizvui attrs).pkgs;
+      nixpkgsArgs.config = {
+        allowUnfree = false;
+        inHydra = true;
+        allowBroken = true;
+      };
+    };
+
+    packagePlatforms = mapAttrs (name: value: let
+      brokenOr = if value.meta.broken or false then const [] else id;
+      platforms = value.meta.hydraPlatforms or (value.meta.platforms or []);
+      isRecursive = value.recurseForDerivations or false
+                 || value.recurseForRelease or false;
+      result = if isDerivation value then brokenOr platforms
+               else if isRecursive then packagePlatforms value
+               else [];
+      tried = builtins.tryEval result;
+    in if tried.success then tried.value else []);
+
+  in with releaseLib; mapTestOn (packagePlatforms releaseLib.pkgs);
+
+in with pkgsUpstream.lib; with builtins; {
+
+  machines = let
+    # We need to expose all the real builds within vuizvui.lazyPackages to make
+    # sure they don't get garbage collected on the Hydra instance.
+    wrapLazy = machine: pkgsUpstream.runCommandLocal machine.build.name {
+      fakeRuntimeDeps = machine.eval.config.vuizvui.lazyPackages;
+      product = machine.build;
+    } ''
+      mkdir -p "$out/nix-support"
+      echo "$product" > "$out/nix-support/fake-runtime-dependencies"
+      for i in $fakeRuntimeDeps; do
+        echo "$i" >> "$out/nix-support/fake-runtime-dependencies"
+      done
+    '';
+  in mapAttrsRecursiveCond (m: !(m ? eval)) (const wrapLazy) allMachines;
+
+  isoImages = let
+    buildIso = attrs: let
+      name = attrs.iso.config.networking.hostName;
+      cond = attrs.iso.config.vuizvui.createISO;
+    in if !cond then {} else pkgsUpstream.runCommandLocal "vuizvui-iso-${name}" {
+      meta.description = "Live CD/USB stick of ${name}";
+      iso = attrs.iso.config.system.build.isoImage;
+      passthru.config = attrs.iso.config;
+    } ''
+      mkdir -p "$out/nix-support"
+      echo "file iso" $iso/iso/*.iso* \
+        >> "$out/nix-support/hydra-build-products"
+    '';
+  in mapAttrsRecursiveCond (m: !(m ? iso)) (const buildIso) allMachines;
+
+  tests = let
+    machineList = collect (m: m ? eval) allMachines;
+    activatedTests = unique (concatMap (machine:
+      machine.eval.config.vuizvui.requiresTests
+    ) machineList);
+    mkTest = path: setAttrByPath path (getAttrFromPath path allTests);
+  in fold recursiveUpdate {} (map mkTest activatedTests) // {
+    inherit (allTests) vuizvui;
+  };
+
+  inherit pkgs;
+
+  channels = let
+    mkChannel = attrs: root.pkgs.mkChannel (rec {
+      name = "vuizvui-channel-${attrs.name or "generic"}-${vuizvuiVersion}";
+      src = vuizvui;
+      patchPhase = ''
+        touch .update-on-nixos-rebuild
+      '';
+    } // removeAttrs attrs [ "name" ]);
+
+    gatherTests = active: map (path: getAttrFromPath path allTests) active;
+
+  in {
+    generic = mkChannel {
+      constituents = concatMap (collect isDerivation) [
+        allTests.vuizvui pkgs
+      ];
+    };
+
+    machines = mapAttrsRecursiveCond (m: !(m ? eval)) (path: attrs: mkChannel {
+      name = "machine-${last path}";
+      constituents = singleton attrs.eval.config.system.build.toplevel
+                  ++ gatherTests attrs.eval.config.vuizvui.requiresTests;
+    }) allMachines;
+  };
+
+  manual = let
+    modules = import "${nixpkgs}/nixos/lib/eval-config.nix" {
+      modules = import "${vuizvui}/modules/module-list.nix";
+      check = false;
+      inherit system;
+    };
+
+    patchedDocbookXSL = overrideDerivation pkgsUpstream.docbook5_xsl (drv: {
+      # Don't chunk off <preface/>
+      postPatch = (drv.postPatch or "") + ''
+        sed -i -e '
+          /<xsl:when.*preface/d
+          /<xsl:for-each/s!|//d:preface \+!!g
+          /<xsl:variable/s!|[a-z]\+::d:preface\[1\] \+!!g
+        ' xhtml/chunk-common.xsl
+
+        sed -i -e '
+          /<xsl:when.*preface/,/<\/xsl:when>/d
+          /<xsl:template/s!|d:preface!!g
+        ' xhtml/chunk-code.xsl
+      '';
+    });
+
+    isVuizvui = opt: head (splitString "." opt.name) == "vuizvui";
+    filterDoc = filter (opt: isVuizvui opt && opt.visible && !opt.internal);
+    optionsXML = toXML (filterDoc (optionAttrSetToDocList modules.options));
+    optionsFile = toFile "options.xml" (unsafeDiscardStringContext optionsXML);
+
+    mkXsltFlags = flags: let
+      mkParam = flag: valFun: opt: val: [ "--${flag}" opt (valFun val) ];
+      mkStrParam = mkParam "stringparam" id;
+      mkBoolParam = mkParam "param" (b: if b then "1" else "0");
+      mkFlag = path: value: let
+        opt = concatStringsSep "." path;
+      in if isString value then mkStrParam opt value
+         else if isBool value then mkBoolParam opt value
+         else throw "Invalid value for '${opt}': ${toString value}";
+      result = collect isList (mapAttrsRecursive mkFlag flags);
+    in concatMapStringsSep " " escapeShellArg (concatLists result);
+
+    xsltFlags = mkXsltFlags {
+      section.autolabel = true;
+      section.label.includes.component.label = true;
+      html.stylesheet = "style.css overrides.css highlightjs/mono-blue.css";
+      html.script = "highlightjs/highlight.pack.js highlightjs/loader.js";
+      xref."with".number.and.title = true;
+      admon.style = "";
+    };
+
+    xsltPath = "${nixpkgs}/nixos/lib/make-options-doc";
+
+  in pkgsUpstream.stdenv.mkDerivation {
+    name = "vuizvui-options";
+
+    nativeBuildInputs = singleton pkgsUpstream.libxslt;
+
+    buildCommand = ''
+      cp -r "${./doc}" doc
+      chmod -R +w doc
+      xsltproc -o intermediate.xml \
+        "${xsltPath}/options-to-docbook.xsl" \
+        ${optionsFile}
+      xsltproc -o doc/options-db.xml \
+        "${xsltPath}/postprocess-option-descriptions.xsl" \
+        intermediate.xml
+
+      dest="$out/share/doc/vuizvui"
+      mkdir -p "$dest"
+
+      xsltproc -o "$dest/" ${xsltFlags} -nonet -xinclude \
+        ${patchedDocbookXSL}/xml/xsl/docbook/xhtml/chunk.xsl \
+        doc/index.xml
+
+      cp "${nixpkgs}/doc/style.css" "$dest/style.css"
+      cp "${nixpkgs}/doc/overrides.css" "$dest/overrides.css"
+      cp -r ${pkgsUpstream.documentation-highlighter} "$dest/highlightjs"
+
+      mkdir -p "$out/nix-support"
+      echo "doc manual $dest" > "$out/nix-support/hydra-build-products"
+    '';
+  };
+}
diff --git a/tests/aszlig/dnyarri/luks2-bcache.nix b/tests/aszlig/dnyarri/luks2-bcache.nix
new file mode 100644
index 00000000..0347a0da
--- /dev/null
+++ b/tests/aszlig/dnyarri/luks2-bcache.nix
@@ -0,0 +1,132 @@
+{
+  name = "dnyarri-luks2-bcache";
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = [
+      pkgs.cryptsetup pkgs.bcache-tools pkgs.btrfs-progs
+    ];
+    virtualisation.memorySize = 2048;
+    virtualisation.emptyDiskImages = [ 5 2048 2048 512 ];
+  };
+
+  nodes.newmachine = { pkgs, lib, ... }: {
+    virtualisation.memorySize = 2048;
+    virtualisation.emptyDiskImages = [ 5 2048 2048 512 ];
+    boot.initrd.luks.devices = lib.mkOverride 0 {
+      test1.device = "/dev/disk/by-uuid/07b821b9-0912-4f03-9ebc-89f41704caff";
+      test1.keyFile = "/dev/vdb";
+      test2.device = "/dev/disk/by-uuid/d140fd40-bb3c-48b5-98e0-b75878dbce66";
+      test2.keyFile = "/dev/vdb";
+    };
+    boot.initrd.availableKernelModules = [ "bcache" ];
+    boot.initrd.postMountCommands = ''
+      test 'hello test' = "$(cat /mnt-root/test/hi.txt)" || exit 1
+    '';
+    fileSystems = lib.mkVMOverride {
+      "/test" = {
+        fsType = "btrfs";
+        label = "testfs";
+        neededForBoot = true;
+      };
+    };
+  };
+
+  testScript = let
+    luksOpts = "--type LUKS2 --pbkdf argon2id -s 512 -h sha512";
+    luksFormat = "cryptsetup luksFormat -q ${luksOpts}";
+    uuid1 = "07b821b9-0912-4f03-9ebc-89f41704caff";
+    uuid2 = "d140fd40-bb3c-48b5-98e0-b75878dbce66";
+  in ''
+    $machine->waitForUnit('multi-user.target');
+
+    $machine->nest('setting up LUKS2 and bcache backing devices', sub {
+      $machine->succeed('dd if=/dev/urandom of=/dev/vdb bs=1 count=200');
+
+      $machine->succeed('make-bcache -B /dev/vdc');
+      $machine->succeed('make-bcache -B /dev/vdd');
+
+      $machine->waitUntilSucceeds(
+        '[ $(echo /dev/bcache[0-9]* | wc -w) -eq 2 ]'
+      );
+      my $bcache1 = $machine->succeed('ls -1 /dev/bcache[0-9]* | head -n1');
+      chomp $bcache1;
+      my $bcache2 = $machine->succeed('ls -1 /dev/bcache[0-9]* | tail -n1');
+      chomp $bcache2;
+
+      $machine->succeed(
+        "${luksFormat} $bcache1 --uuid ${uuid1} /dev/vdb",
+        "cryptsetup open $bcache1 l1 --key-file=/dev/vdb",
+      );
+
+      $machine->succeed(
+        "${luksFormat} $bcache2 --uuid ${uuid2} /dev/vdb",
+        "cryptsetup open $bcache2 l2 --key-file=/dev/vdb",
+      );
+
+      $machine->succeed(
+        'mkfs.btrfs -L testfs -m raid1 -d raid1 /dev/mapper/l1 /dev/mapper/l2',
+        'btrfs dev scan',
+        'mkdir /mnt-test',
+        'mount /dev/disk/by-label/testfs /mnt-test',
+        'echo hello test > /mnt-test/hi.txt',
+        'umount /mnt-test',
+        'cryptsetup close l1',
+        'cryptsetup close l2',
+      );
+    });
+
+    $machine->nest('rebooting into new configuration', sub {
+      $machine->shutdown;
+      $newmachine->{stateDir} = $machine->{stateDir};
+      $newmachine->waitForUnit('multi-user.target');
+    });
+
+    my $bcache1 =
+      $newmachine->succeed('cd /dev; ls -1 bcache[0-9]* | head -n1');
+    chomp $bcache1;
+    my $bcache2 =
+      $newmachine->succeed('cd /dev; ls -1 bcache[0-9]* | tail -n1');
+    chomp $bcache2;
+
+    $machine->nest('attaching bcache cache device', sub {
+      my $csetuuid = $newmachine->succeed(
+        'make-bcache -C /dev/vde | sed -n -e "s/^Set UUID:[^a-f0-9]*//p"'
+      );
+      chomp $csetuuid;
+
+      $newmachine->nest('wait for cache device to appear', sub {
+        $newmachine->waitUntilSucceeds("test -e /sys/fs/bcache/$csetuuid");
+      });
+
+      $newmachine->succeed(
+        "echo $csetuuid > /sys/block/$bcache1/bcache/attach",
+        "echo writeback > /sys/block/$bcache1/bcache/cache_mode",
+        "echo $csetuuid > /sys/block/$bcache2/bcache/attach",
+        "echo writeback > /sys/block/$bcache2/bcache/cache_mode"
+      );
+    });
+
+    $machine->nest('write random files to test file system', sub {
+      $newmachine->succeed(
+        'for i in $(seq 100); do'.
+        ' dd if=/dev/urandom of="/test/randfile.$i" bs=1 count=100;'.
+        ' sha256sum "/test/randfile.$i" > "/test/randfile.$i.sha256"; '.
+        'done'
+      );
+    });
+
+    $machine->nest('reboot to clear disk buffers', sub {
+      $newmachine->shutdown;
+      $newmachine->waitForUnit('multi-user.target');
+    });
+
+    $machine->nest('verifying contents of random files created earlier', sub {
+      $newmachine->succeed(
+        'for i in $(seq 100); do'.
+        ' sha256sum "/test/randfile.$i" | cmp - "/test/randfile.$i.sha256"'.
+        ' || exit 1; '.
+        'done'
+      );
+    });
+  '';
+}
diff --git a/tests/aszlig/programs/psi.nix b/tests/aszlig/programs/psi.nix
new file mode 100644
index 00000000..0b6643f8
--- /dev/null
+++ b/tests/aszlig/programs/psi.nix
@@ -0,0 +1,25 @@
+{ nixpkgsPath, ... }:
+
+{
+  name = "psi-test";
+
+  machine = { pkgs, ... }: {
+    imports = [
+      "${nixpkgsPath}/nixos/tests/common/user-account.nix"
+      "${nixpkgsPath}/nixos/tests/common/x11.nix"
+    ];
+    test-support.displayManager.auto.user = "alice";
+    environment.systemPackages = [ pkgs.vuizvui.aszlig.psi ];
+  };
+
+  enableOCR = true;
+
+  testScript = ''
+    $machine->waitForX;
+    $machine->waitForFile("/home/alice/.Xauthority");
+    $machine->succeed("xauth merge ~alice/.Xauthority");
+    $machine->succeed('su -c "DISPLAY=:0.0 psi" - alice &');
+    $machine->waitForText(qr/Register new account/i);
+    $machine->screenshot('psi');
+  '';
+}
diff --git a/tests/default.nix b/tests/default.nix
new file mode 100644
index 00000000..edc022ad
--- /dev/null
+++ b/tests/default.nix
@@ -0,0 +1,24 @@
+{ system ? builtins.currentSystem
+, nixpkgsPath ? import ../nixpkgs-path.nix
+, ...
+}:
+
+let
+  callTest = path: import ./make-test.nix (import path) {
+    inherit system nixpkgsPath;
+  };
+
+in {
+  aszlig.dnyarri.luks2-bcache = callTest ./aszlig/dnyarri/luks2-bcache.nix;
+  aszlig.programs.psi = callTest aszlig/programs/psi.nix;
+  games = {
+    starbound = callTest ./games/starbound.nix;
+  };
+  programs = {
+    gnupg = callTest ./programs/gnupg;
+  };
+  sandbox = callTest ./sandbox.nix;
+  system = {
+    kernel.bfq = callTest ./system/kernel/bfq.nix;
+  };
+}
diff --git a/tests/games/starbound.nix b/tests/games/starbound.nix
new file mode 100644
index 00000000..d1ef15e1
--- /dev/null
+++ b/tests/games/starbound.nix
@@ -0,0 +1,103 @@
+{ pkgs, nixpkgsPath, ... }:
+
+let
+  xdo = { name, description, xdoScript }: let
+    xdoFile = pkgs.writeText "${name}.xdo" ''
+      search --onlyvisible --class starbound
+      windowfocus --sync
+      windowactivate --sync
+      ${xdoScript}
+    '';
+    escapeScreenshot = pkgs.lib.replaceStrings ["-"] ["_"];
+  in ''
+    $client->nest("${description}", sub {
+      $client->screenshot("before_${escapeScreenshot name}");
+      $client->succeed("${pkgs.xdotool}/bin/xdotool '${xdoFile}'");
+    });
+  '';
+
+  clickAt = name: x: y: xdo {
+    name = "click-${name}";
+    description = "clicking on ${name} (coords ${toString x} ${toString y})";
+    xdoScript = ''
+      mousemove --window %1 --sync ${toString x} ${toString y}
+      click --repeat 10 1
+    '';
+  };
+
+  typeText = name: text: xdo {
+    name = "type-${name}";
+    description = "typing `${text}' into Starbound";
+    xdoScript = ''
+      type --delay 200 '${text}'
+    '';
+  };
+
+in {
+  name = "starbound";
+
+  enableOCR = true;
+
+  nodes = {
+    server = {
+      vuizvui.services.starbound = {
+        enable = true;
+        # Use a different dataDir than the default to make
+        # sure everything is still working.
+        dataDir = "/var/lib/starbound-test";
+        users.alice.password = "secret";
+      };
+      virtualisation.memorySize = 2047;
+      networking.interfaces.eth1.ipAddress = "192.168.0.1";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+
+    client = { pkgs, ... }: {
+      imports = [
+        "${nixpkgsPath}/nixos/tests/common/x11.nix"
+      ];
+      virtualisation.memorySize = 2047;
+      environment.systemPackages = [
+        pkgs.vuizvui.games.humblebundle.starbound
+      ];
+      networking.interfaces.eth1.ipAddress = "192.168.0.2";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = ''
+    $server->waitForUnit("starbound.service");
+
+    $client->nest("waiting for client to start up", sub {
+      $client->waitForX;
+      $client->succeed("starbound >&2 &");
+      $client->waitForText(qr/options/i);
+    });
+
+    ${clickAt "join-game" 100 560}
+    $client->waitForText(qr/select/i);
+    ${clickAt "new-character" 460 220}
+    $client->waitForText(qr/randomise/i);
+    ${clickAt "create-character" 600 625}
+    $client->waitForText(qr/select/i);
+    ${clickAt "use-character" 460 220}
+    $client->waitForText(qr/ser[vu]er/i);
+
+    ${clickAt "server-address" 460 322}
+    ${typeText "server-address" "192.168.0.1"}
+
+    ${clickAt "server-account" 490 354}
+    ${typeText "server-account" "alice"}
+
+    ${clickAt "server-password" 490 386}
+    ${typeText "server-password" "secret"}
+
+    ${clickAt "join-server" 495 420}
+
+    $client->waitForText(qr/graduation/i);
+    $client->sleep(30);
+    $client->screenshot("client");
+  '';
+}
diff --git a/tests/make-test.nix b/tests/make-test.nix
new file mode 100644
index 00000000..cdc763c7
--- /dev/null
+++ b/tests/make-test.nix
@@ -0,0 +1,35 @@
+testFun:
+
+{ system ? builtins.currentSystem
+, nixpkgsPath ? import ../nixpkgs-path.nix
+, ...
+}@args: let
+
+  lib = import "${nixpkgsPath}/lib";
+
+  pkgs = import nixpkgsPath { inherit system; };
+
+  testLib = import "${nixpkgsPath}/nixos/lib/testing.nix" {
+    inherit pkgs system;
+  };
+
+  testArgs = if builtins.isFunction testFun then testFun (args // {
+    pkgs = pkgs // {
+      vuizvui = import ../pkgs { inherit pkgs; };
+    };
+    inherit nixpkgsPath;
+  }) else testFun;
+
+  nodes = testArgs.nodes or (if testArgs ? machine then {
+    inherit (testArgs) machine;
+  } else {});
+
+  injectCommon = name: conf: {
+    imports = [ conf ] ++ import ../modules/module-list.nix;
+  };
+
+  testArgsWithCommon = removeAttrs testArgs [ "machine" ] // {
+    nodes = lib.mapAttrs injectCommon nodes;
+  };
+
+in testLib.makeTest testArgsWithCommon
diff --git a/tests/programs/gnupg/default.nix b/tests/programs/gnupg/default.nix
new file mode 100644
index 00000000..504f6e46
--- /dev/null
+++ b/tests/programs/gnupg/default.nix
@@ -0,0 +1,136 @@
+{ pkgs, nixpkgsPath, ... }:
+
+let
+  mkExpect = expectScript: script: pkgs.writeScript "test-gnupg-cli" ''
+    #!${pkgs.expect}/bin/expect -f
+    set timeout 20
+    spawn ${pkgs.writeScript "cli-testscript.sh" ''
+      #!${pkgs.stdenv.shell} -e
+      ${script}
+    ''}
+    ${expectScript}
+    set ret [wait]
+    exit [lindex $ret 3]
+  '';
+
+  cliTestWithPassphrase = mkExpect ''
+    expect -regexp ---+.*Please.enter
+    send supersecret\r
+  '';
+
+  cliTest = mkExpect "";
+
+in {
+  name = "gnupg";
+
+  enableOCR = true;
+
+  machine = { lib, ... }: {
+    imports = map (what:
+      "${nixpkgsPath}/nixos/tests/common/${what}.nix"
+    ) [ "user-account" "x11" ];
+
+    services.openssh.enable = true;
+    test-support.displayManager.auto.user = "alice";
+
+    vuizvui.programs.gnupg.enable = true;
+    vuizvui.programs.gnupg.agent.enable = true;
+    vuizvui.programs.gnupg.agent.sshSupport = true;
+    vuizvui.programs.gnupg.agent.scdaemon.enable = true;
+
+    programs.ssh.startAgent = false;
+  };
+
+  testScript = ''
+    $machine->waitForUnit("sshd.service");
+    $machine->succeed("ssh-keygen -t ed25519 -f /root/id_ed25519 -N '''");
+    my $cmd = 'mkdir -p ~/.ssh && cat > ~/.ssh/authorized_keys';
+    $machine->succeed("su -c 'umask 0077; $cmd' alice < /root/id_ed25519.pub");
+
+    $machine->waitForX;
+
+    sub ssh ($) {
+      my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
+      return "ssh -q -i /root/id_ed25519".
+             " -o StrictHostKeyChecking=no".
+             " alice\@127.0.0.1 -- '$esc'";
+    }
+
+    sub xsu ($) {
+      my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
+      return "DISPLAY=:0 su alice -c '$esc'";
+    }
+
+    $machine->nest("import snakeoil key", sub {
+      $machine->succeed(ssh "${cliTestWithPassphrase ''
+        gpg --import ${./snakeoil.asc}
+      ''}");
+      $machine->succeed(ssh "${mkExpect ''
+        expect gpg>
+        send trust\r
+        expect decision?
+        send 5\r
+        expect "Do you really want"
+        send y\r
+        expect gpg>
+        send save\r
+      '' "gpg --edit-key ECC15FE1"}");
+    });
+
+    subtest "test SSH agent support", sub {
+      $machine->succeed(ssh 'ssh-keygen -t ed25519 -f ~/testkey -N ""');
+      $machine->succeed(ssh '${mkExpect ''
+        expect -regexp ---+.*Please.enter
+        send supersecret\r
+        expect -regexp ---+.*Please.re-en
+        send supersecret\r
+      '' "ssh-add ~/testkey"}');
+
+      $machine->succeed("umask 0077; $cmd < ~alice/testkey.pub");
+      $machine->succeed(ssh 'rm ~/testkey*');
+
+      $machine->succeed(ssh 'ssh -o StrictHostKeyChecking=no root@127.0.0.1'.
+                            ' touch /i_have_thu_powarr');
+      $machine->succeed("test -e /i_have_thu_powarr");
+
+      $machine->succeed(ssh "systemctl --user reload gpg-agent");
+
+      $machine->succeed(ssh "${cliTestWithPassphrase ''
+        ssh -o StrictHostKeyChecking=no root@127.0.0.1 \
+          touch /i_still_have_thu_powarr
+      ''}");
+      $machine->succeed("test -e /i_still_have_thu_powarr");
+    };
+
+    subtest "socket persists after restart", sub {
+      $machine->succeed(ssh 'test -e "$SSH_AUTH_SOCK"');
+      $machine->succeed(ssh 'systemctl --user stop gpg-agent.service');
+      $machine->succeed(ssh 'test -e "$SSH_AUTH_SOCK"');
+    };
+
+    subtest "test from SSH", sub {
+      $machine->execute(ssh "systemctl --user reload gpg-agent");
+      $machine->succeed(ssh "${cliTestWithPassphrase ''
+        echo encrypt me > to_encrypt
+        gpg -sea -r ECC15FE1 to_encrypt
+        rm to_encrypt
+      ''}");
+      $machine->succeed(ssh "${cliTest ''
+        [ "$(gpg -d to_encrypt.asc)" = "encrypt me" ]
+      ''}");
+    };
+
+    subtest "test from X", sub {
+      $machine->execute(ssh "systemctl --user reload gpg-agent");
+      my $pid = $machine->succeed(xsu
+        'echo encrypt me | gpg -sea -r ECC15FE1 > encrypted_x.asc & echo $!'
+      );
+      chomp $pid;
+      $machine->waitForText(qr/[Pp]assphrase/);
+      $machine->screenshot("passphrase_dialog");
+      $machine->sendChars("supersecret\n");
+      $machine->waitUntilFails("kill -0 $pid");
+      $machine->succeed(xsu '[ "$(gpg -d encrypted_x.asc)" = "encrypt me" ]');
+    };
+  '';
+}
diff --git a/tests/programs/gnupg/snakeoil.asc b/tests/programs/gnupg/snakeoil.asc
new file mode 100644
index 00000000..59c07011
--- /dev/null
+++ b/tests/programs/gnupg/snakeoil.asc
@@ -0,0 +1,59 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+lQO+BFb12VYBCADBxfyzvHKtc5L2b9tqw5oOgAxnAWnsj5Weapm/zlK+gd32/PIy
+LN++ZBoxJDr5geSU8vdoI6aKAP8zhOlWU9B+vE83cDuCtvLaR7DiWxXpvACr+2pL
+Hd9ZUDVGC8HGJOljpqF04rkyHFvWIksQz2ihGR616kR3Ir2YOnGkiefsREnS/CF3
+1GXYfg4w9YO77GdCMAdXJ1I3PH+axkHjveWDKFD5f31dcolAqChl2zMoFXkPLnrf
+tA91his15YJFTIjt9KIA++J+2VEtOPvUqC6yI+DlS+j3Ie2BPi1yo10PG9TR//WI
+r2jQ36AvON87ZVNsA0YOQiZUbbS8NeUx+Y6NABEBAAH+AwMC8LL9GSjcywXbhmNt
+SMvlVHJwECg1pu/+VD0F+PTg6zXIYTeIoM2QZxxFsN2ugC8d7jfn15qX843c0npu
+hP8OeCv62pyAdSIaE8tLczPHjy613w67S4DSazaGjMA6ED/YyHOimi6Iz7+GYksZ
+DwNRe2jULr15+yVgLDXpL6Z+ROZDK6i8ovR0VZ6ueINISza3TYgsm9j/rCMbtjCh
+Ut6I4e6Ja8nJgTwwN8WezTcpo1QGBS8x0C4SYC3rDLYjlYidOXQX4OfzAYO66ABd
+/g3+NeKEFRT7EBoZgiwYX8jXhJiU14H0ZmJl5donKjZURD+kZEYj0oS6Q8VhHGfP
+eqVj5O09RRYLa9aAln/6C2J/FHDz0FhPkojISKximPN2ATxypBMweyTPuMBYKVcj
+52Dzj2crrZeTfVDmJojuM/enz7jJ2VyUsCF+V6x6Zgj3PYJEsw55C2elLNhQg9No
+GN4QXpiC3bArrEINQpcZy0Nhr56HHIBuIvLY39h0uNJFmtwog9lyyW+iG9snE2rp
+kmwd8aglH2VZhtE5SV4D/Hf9raDrrP4sLNWTeDF9vmJZ/gdnGwVYNaAfDlxyyReR
+ptqnJ8Q3mm4pQ65zKFG89UOw8ZmUVkofgdMOAAGNjVRMPkQAIu2O1oJVfAGJT+gv
+G2tplAFgMbRqpjOlL3Rvh8K3gNeA1iwa4Na9qZfo/GcmvJ150zi7TBWcT2EmOyJc
+xUMMQgXTm5JJkY/fDw75Fv7FogN5VkG2Uf8+kkfW++zkT4kF8yyD4Lw2JUuUn5l5
+JWsXDmN6fK0vhXInubkmyV2DXmsS2YVTPmtZvIYa2nVvdamh6QDwUULmnI1VPqdk
+/i1v8dMkoV5eV91pir9N6JcWng5OKz1DAY1X8fWH9bbCD0Rf/xpCbJoRSchEQqqM
+W7QZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPokBOQQTAQgAIwUCVvXZVgIbAwcL
+CQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJENWKNpXswV/hbnMH/35mgiPd9prX
+YqRrylVyxSiHdewVeR8nghjo/g2tR9D/A9feGoz3WL24J3NINAVmZZzKnvpT9Ut+
+Nzy5vL111TSSkMdYIrcjMu4/iUoc8w2JFExMeg1JI1EOS7ctd3qOWMYeWHtlzEJS
+DORsR/IGqq8KHNKtJPywpcjSCpXtiqzjjJrE8F2SbYMFY21SBza6QQY+Vlerr+Bo
+fwa8f3z+cyr0ISHHtEI1h8KoCCWTp/YU1FIEYc22CGz80ExMgCbBxWYukn709Wxd
+6QTFqmNtNUHi4xq1zOA/m0JMASdZPzbRcsbUQGWlwW85Dq4jYV08kC4mPLW537Lx
+A3+rzT5aiB+dA74EVvXZVgEIALXQn98p0mzZYki0aFkS5APQ1gpuXcsMRqlGQTd+
+6gZF32yEWMRrQO8gs59T4zZjGa1EhrMStMHdApxYw82oxhUU8krjYkhqOxZyW363
+H+MTYohiwr3Q6YEdVm6E8lcZwHE2d3WD5bdS0JsDjlZXMXjcJ1bivmwGGAWaucxi
+jAOayYTRpSKUFZDiFTln5dmiFFejyhU/jkTYm7VtXOQbNTwsUCkQtxT8Z468x7M6
+GzDdEvRDgN4VMVbJ2IWQdgS1WaAP9GvZgjS5B2yKUA6ONlOQOdF6gZChr2ej6Jue
+P/feNiuF9ZEqzwB1t1RrljGoyL0jjMH7RCJo2iy/OcL/nocAEQEAAf4DAwLwsv0Z
+KNzLBdvWJwkmTkAjVJRk779nD95vjddWFZgT0zy43U7AyiCYITHms0+/TM3qI5Yt
+teLBARbRddHz3+Wp6ed9zFHlCZW89Qa1yfmSsPFdp+UyN+SVHsaQIGZmFDPQ5uEd
+JRMwgnI5k09APCIq5YCE6bDcvcVLEBFT9IsuY6oWB8FLjh4fe+WAZxDlePHCxf7H
+jAfe4RDiN+bKEZQruGIfhwyuehQW/SOzY6L9PnNfouVWq5nUAl4oxGwsJfhyMpte
+MhqXox5uEeLn8S4gWZtD57Ux8CQAtAZccvjWG5jZXa2bNaEpIRBZGL6r0TS0aKTG
+v2n3CThLsYEudMiWzB7+l74ANFggZnMBXsc2nSElg57GjaCygFkpHnGeghiOjL/9
+cj/yHRz1SKH18lI+Uet/i/QFoHCGeZFbtQ8RUSp93meCHzsFKQ2ZG+djK8HqV5T0
+Tfov1RuHD9RyU00Ohc3RJWSTyeMjxAgjhJKnnfEb1w2JMcXbBCakudBAAMa2Sbdw
+a7h1I+IVTLr9SWRLYg1bWR1hCKjrjBGTA09VZF8BAH1yrszKxOPovV/fLNjohDd5
+xUXu96amSVDhq0M1DVFu8gEADN80+FhUYXIZs1HSoXuw8gusd2Bjq12oyaKNEVd0
+gazgrZ83uAT3PTkEtD4UKjCURPXJ/b4IeQlwkehcwGT7cWhgt8waNPSU5+majRXa
+RJZ/nqdk41E+NN2RvkIuyxl3ggosc3g8jtr8h2115JnoRmGzoZThrhceqVa9aLUd
+Cf6EIoXxL5RPRwaAkimuOEflHEx0NetRNVCIqhq7GLyc4LVMGhTi5U+XAg95X6gJ
+LzvVtrx3P7XG/gd74nAAW5MnW9sVXiuZZzfD56Fl7h79wAg7k3refnbERNSP1WEL
+hmUPS9SW/cKUiQEfBBgBCAAJBQJW9dlWAhsMAAoJENWKNpXswV/h5UAH/itFIGwr
+p7taEh9+x23vPdw0IuKl2lRmx4QIIC55AlzU1Tlij3jppz8PgfLArJDBY9cLe2ir
+cxXIEf+/L59832Q1Z09OXTElqpLw82wWjxTN4b4ZQjgkHGwO4RgxQKdvwDpWVt6g
+JaI1d4LAyW/RxF1vvtC4OzoUtjNXxPLHzga0PP9TOhpuPSB0fc4FDU9QaLUemkJZ
+VUICqAOcTQpENMHdDJcizYsahca2bg5gYaV1Tv/sNINNxKqcSGb1iUdJz4hAaRmO
++y4+aKxJkyt+WqmUOa5aZ9D3s9P87IuSNMc51lgiBFKWBrqSQCTfLBxMbSsPZk9h
+75FOlpj5VS82Sl0=
+=3HD3
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/sandbox.nix b/tests/sandbox.nix
new file mode 100644
index 00000000..66187b23
--- /dev/null
+++ b/tests/sandbox.nix
@@ -0,0 +1,140 @@
+{
+  name = "sandbox";
+
+  machine = { pkgs, lib, ... }: {
+    system.activationScripts.inject-link = ''
+      ln -svf ${pkgs.hello} /run/foo-test-sandbox
+      ln -svf ${pkgs.gnused} /run/bar-test-sandbox
+      ln -svf ${pkgs.gnugrep} /run/baz-test-sandbox
+    '';
+    environment.sessionVariables.COLLECT_ME = [
+      "/run/foo-test-sandbox"
+      "/run/bar-test-sandbox"
+      "/run/baz-test-sandbox"
+    ];
+
+    # Only needed so we get the right XDG paths in the system path.
+    services.xserver.enable = true;
+    systemd.services.display-manager.enable = false;
+
+    environment.systemPackages = let
+      mkNestedLinksTo = drv: let
+        mkLink = name: to: pkgs.runCommandLocal name { inherit to; } ''
+          ln -s "$to" "$out"
+        '';
+      in mkLink "nested-1" (mkLink "nested-2" (mkLink "nested-3" drv));
+
+      testPackage = pkgs.runCommand "test-sandbox" {
+        program = ''
+          #!${pkgs.stdenv.shell} -ex
+
+          if [ "$1" != canary ]; then
+            echo 'Canary check failed, so the test program probably' \
+                 'was not executed via the XDG desktop entry.' >&2
+            exit 1
+          fi
+
+          # Should fail because we can't access the host's PATH
+          ! echo foo | grep -qF foo
+
+          # No /bin/sh by default
+          test ! -e /bin
+          test ! -e /bin/sh
+
+          # Write PID information to files, so that we can later verify whether
+          # we were in a PID namespace.
+          echo $$ > /home/foo/.cache/xdg/ownpid
+          ls -d1 /proc/[0-9]* > /home/foo/.cache/xdg/procpids
+
+          # Check whether we can access files behind nested storepaths that are
+          # symlinks.
+          lfile="$(< ${mkNestedLinksTo (pkgs.writeText "target" "file")})"
+          test "$lfile" = file
+          ldir="$(< ${mkNestedLinksTo (pkgs.runCommandLocal "target" {} ''
+            mkdir -p "$out"
+            echo dir > "$out/canary"
+          '')}/canary)"
+          test "$ldir" = dir
+
+          export PATH=/run/baz-test-sandbox/bin
+          echo foo > /home/foo/existing/bar
+          test ! -d /home/foo/nonexisting
+          /run/foo-test-sandbox/bin/hello
+          echo aaa | /run/bar-test-sandbox/bin/sed -e 's/a/b/g'
+
+          echo XDG1 > /home/foo/.local/share/xdg/1
+          echo XDG2 > /home/foo/.config/xdg/2
+          echo XDG3 > /home/foo/.cache/xdg/3
+          echo > /home/foo/.cache/xdg/done
+        '';
+      } ''
+        mkdir -p "$out/bin" "$out/share/applications" "$out/share/test-sandbox"
+
+        echo -n "$program" > "$out/bin/test-sandbox"
+        chmod +x "$out/bin/test-sandbox"
+
+        echo '<svg xmlns="http://www.w3.org/2000/svg"/>' \
+          > "$out/share/test-sandbox/icon.svg"
+
+        cat > "$out/share/applications/test.desktop" <<EOF
+        [Desktop Entry]
+        Name=$fullName
+        Type=Application
+        Version=1.1
+        Exec=$out/bin/test-sandbox canary
+        Icon=$out/share/test-sandbox/icon.svg
+        Categories=Utility
+        EOF
+      '';
+
+    in [
+      # Unfortunately, "xdg-open test-sandbox.desktop" doesn't work, so let's
+      # use gtk-launch instead. We also need xvfb_run so that we can avoid to
+      # start a full-blown X server.
+      #
+      # See also:
+      #
+      #   https://askubuntu.com/questions/5172
+      #   https://bugs.launchpad.net/ubuntu/+source/gvfs/+bug/378783
+      #
+      (lib.getBin pkgs.gtk3) pkgs.xvfb_run
+
+      (pkgs.vuizvui.buildSandbox testPackage {
+        paths.required = [
+          "/home/foo/existing"
+          "$XDG_DATA_HOME/xdg"
+          "$XDG_CONFIG_HOME/xdg"
+          "$XDG_CACHE_HOME/xdg"
+        ];
+        paths.wanted = [ "/home/foo/nonexisting" ];
+        paths.runtimeVars = [ "COLLECT_ME" ];
+      })
+
+      (pkgs.vuizvui.buildSandbox (pkgs.writeScriptBin "test-sandbox2" ''
+        #!/bin/sh
+        # Another /bin/sh just to be sure :-)
+        /bin/sh -c 'echo /bin/sh works'
+      '') { allowBinSh = true; })
+    ];
+    users.users.foo.isNormalUser = true;
+  };
+
+  testScript = ''
+    $machine->waitForUnit('multi-user.target');
+    $machine->succeed('su - -c "xvfb-run gtk-launch test" foo >&2');
+    $machine->waitForFile('/home/foo/.cache/xdg/done');
+
+    $machine->succeed('test -d /home/foo/existing');
+    $machine->succeed('grep -qF foo /home/foo/existing/bar');
+    $machine->fail('test -d /home/foo/nonexisting');
+
+    $machine->succeed('grep -qF XDG1 /home/foo/.local/share/xdg/1');
+    $machine->succeed('grep -qF XDG2 /home/foo/.config/xdg/2');
+    $machine->succeed('grep -qF XDG3 /home/foo/.cache/xdg/3');
+
+    $machine->succeed('test "$(< /home/foo/.cache/xdg/procpids)" = /proc/1');
+    $machine->succeed('test "$(< /home/foo/.cache/xdg/ownpid)" = 1');
+
+    $machine->succeed('test "$(su -c test-sandbox2 foo)" = "/bin/sh works"');
+  '';
+}
diff --git a/tests/system/kernel/bfq.nix b/tests/system/kernel/bfq.nix
new file mode 100644
index 00000000..8a76d2a0
--- /dev/null
+++ b/tests/system/kernel/bfq.nix
@@ -0,0 +1,14 @@
+{
+  name = "bfq-kernel";
+
+  machine = { pkgs, ... }: {
+    boot.kernelPackages = pkgs.linuxPackages_latest;
+    vuizvui.system.kernel.bfq.enable = true;
+    virtualisation.qemu.diskInterface = "scsi";
+  };
+
+  testScript = ''
+    $machine->execute('tail /sys/block/*/queue/scheduler >&2');
+    $machine->succeed('grep -HF "[bfq]" /sys/block/sda/queue/scheduler');
+  '';
+}