about summary refs log tree commit diff
path: root/pkgs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs')
-rw-r--r--pkgs/aszlig/default.nix2
-rw-r--r--pkgs/aszlig/psi/config.patch278
-rw-r--r--pkgs/aszlig/psi/darkstyle.patch32
-rw-r--r--pkgs/aszlig/psi/default.nix72
-rw-r--r--pkgs/aszlig/psi/disable-xep-0232.patch50
-rw-r--r--pkgs/aszlig/santander/default.nix98
-rw-r--r--pkgs/aszlig/santander/pipelight.patch13
-rw-r--r--pkgs/aszlig/santander/winscard.patch11
-rw-r--r--pkgs/aszlig/vim/default.nix33
-rw-r--r--pkgs/build-support/build-sandbox/default.nix42
-rw-r--r--pkgs/build-support/build-sandbox/src/Makefile17
-rw-r--r--pkgs/build-support/build-sandbox/src/setup.c34
-rw-r--r--pkgs/games/gog/crosscode.nix4
-rw-r--r--pkgs/games/gog/default.nix4
-rw-r--r--pkgs/games/gog/fetch-gog/default.nix40
-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/the-longest-journey/default.nix106
-rw-r--r--pkgs/games/gog/the-longest-journey/predefined-config.patch29
-rw-r--r--pkgs/games/gog/warcraft2/default.nix148
-rw-r--r--pkgs/games/gog/warcraft2/xdg.patch37
-rw-r--r--pkgs/games/humblebundle/default.nix1
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/default.nix91
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch121
-rw-r--r--pkgs/games/humblebundle/minimetro.nix15
-rw-r--r--pkgs/games/itch/fetch-itch/default.nix1
-rw-r--r--pkgs/games/steam/fetchsteam/default.nix1
-rw-r--r--pkgs/profpatsch/default.nix94
-rw-r--r--pkgs/profpatsch/display-infos/default.nix50
-rw-r--r--pkgs/profpatsch/execline/escape.nix30
-rw-r--r--pkgs/profpatsch/execline/run-execline-tests.nix82
-rw-r--r--pkgs/profpatsch/execline/run-execline.nix19
-rw-r--r--pkgs/profpatsch/execline/symlink.nix48
-rw-r--r--pkgs/profpatsch/nman/default.nix4
-rw-r--r--pkgs/profpatsch/sfttime/default.nix14
-rwxr-xr-xpkgs/profpatsch/sfttime/sfttime.sh145
-rw-r--r--pkgs/profpatsch/utils-hs/default.nix14
-rw-r--r--pkgs/profpatsch/xmonad/DhallTypedInput.hs232
39 files changed, 1875 insertions, 284 deletions
diff --git a/pkgs/aszlig/default.nix b/pkgs/aszlig/default.nix
index 70c6ff9d..d1dbcb73 100644
--- a/pkgs/aszlig/default.nix
+++ b/pkgs/aszlig/default.nix
@@ -8,8 +8,8 @@
   grandpa = callPackage ./grandpa { };
   librxtx_java = callPackage ./librxtx-java { };
   lockdev = callPackage ./lockdev { };
+  psi = callPackage ./psi { };
   pvolctrl = callPackage ./pvolctrl { };
-  santander = callPackage_i686 ./santander { };
   vim = callPackage ./vim { vim = vim_configurable; };
   xournal = callPackage ./xournal { inherit xournal; };
 }
diff --git a/pkgs/aszlig/psi/config.patch b/pkgs/aszlig/psi/config.patch
new file mode 100644
index 00000000..a9462102
--- /dev/null
+++ b/pkgs/aszlig/psi/config.patch
@@ -0,0 +1,278 @@
+diff --git a/options/default.xml b/options/default.xml
+index 90f019b3..ef33997b 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">
+@@ -86,7 +86,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>
+@@ -128,7 +128,7 @@
+                 <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>
+@@ -146,8 +146,8 @@
+                 <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>
+@@ -208,7 +208,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>
+@@ -223,7 +223,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>
+@@ -248,7 +248,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>
+@@ -285,7 +285,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>
+@@ -335,7 +335,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>
+@@ -353,8 +353,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>
+@@ -364,16 +364,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" >
+@@ -384,21 +384,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>
+@@ -411,10 +411,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>
+@@ -462,20 +462,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">false</successful-subscription>
+@@ -494,7 +494,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>
+@@ -627,7 +627,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>
+@@ -650,7 +650,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>
+@@ -690,5 +703,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 77fbd169..4b4bca11 100644
+--- a/src/psi_profiles.cpp
++++ b/src/psi_profiles.cpp
+@@ -79,8 +79,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;
+@@ -90,7 +90,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;
+@@ -133,7 +133,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..45c5067f
--- /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
+     ${PROJECT_SOURCE_DIR}/iconsets.qrc
++    ${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..d0e24c05
--- /dev/null
+++ b/pkgs/aszlig/psi/default.nix
@@ -0,0 +1,72 @@
+{ 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.0git20190922aszlig";
+
+  src = fetchFromGitHub {
+    owner = "psi-im";
+    repo = "psi";
+    rev = "af26ce1c04207d384a05ea530d571068310957c6";
+    sha256 = "0pxd4ha391mfmsa7n7ag5kqw0nv825wsnkyfxi8wsa942bnbircg";
+    fetchSubmodules = true;
+  };
+
+  plugins = fetchFromGitHub {
+    owner = "psi-im";
+    repo = "plugins";
+    rev = "5574afcc8ab4c5647831d38be111976fb1fa10d3";
+    sha256 = "1h011j94iy40ksiqlprjamfyv7irql502hhhb0mpabk4mndxmjgn";
+  };
+
+  patches = [
+    ./disable-xep-0232.patch
+    ./darkstyle.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-xep-0232.patch b/pkgs/aszlig/psi/disable-xep-0232.patch
new file mode 100644
index 00000000..4788d262
--- /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 d8573bf..31af799 100644
+--- a/iris/src/xmpp/xmpp-im/client.cpp
++++ b/iris/src/xmpp/xmpp-im/client.cpp
+@@ -1257,45 +1257,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/santander/default.nix b/pkgs/aszlig/santander/default.nix
deleted file mode 100644
index adac0a8d..00000000
--- a/pkgs/aszlig/santander/default.nix
+++ /dev/null
@@ -1,98 +0,0 @@
-{ stdenv, lib, fetchurl, fetchgit, fetchpatch, runCommand, p7zip, jq
-, winePackages, pcsclite
-}:
-
-let
-  patchedWine = let
-    libpcsclite = "${lib.getLib pcsclite}/lib/libpcsclite.so";
-  in winePackages.minimal.overrideAttrs (drv: {
-    scard4wine = fetchgit {
-      url = "git://git.code.sf.net/p/scard4wine/code";
-      rev = "c14c02c80bf1f2bb4cedd1f53a3a2ab9c48bed76";
-      sha256 = "0ffmbl9mdnaih4h3ggpnzqbih3kgbwl3wv6j1ag5s4czn8gcpdq3";
-    };
-
-    prePatch = (drv.prePatch or "") + ''
-      cp -t dlls/winscard "$scard4wine/src/"*
-      sed -i -re 's,"libpcsclite\.so(\.[0-9]+)*","${libpcsclite}",' \
-        dlls/winscard/winscard.c
-    '';
-
-    patches = (drv.patches or []) ++ [
-      ./winscard.patch
-      (fetchpatch {
-        url = "http://achurch.org/patch-pile/wine/3.0/disable-unixfs.diff";
-        sha256 = "1yj3walwalya9g9aajcp4iygh348npp9dmks66r9dvwbd3fa8wcb";
-      })
-    ];
-
-    configureFlags = lib.toList (drv.configureFlags or []) ++ [
-      "--disable-unixfs"
-    ];
-
-    postConfigure = (drv.postConfigure or "") + ''
-      # The wineprefix is within the Nix store, so let's ensure wine doesn't
-      # check the owner of the files:
-      sed -i -e '/HAVE_GETUID/d' include/config.h
-    '';
-  });
-
-in stdenv.mkDerivation rec {
-  name = "TRAVIC-Sign-${version}";
-  version = "3.1.3.0";
-
-  src = fetchurl {
-    url = "https://service.santanderbank.de/special/banking/files/"
-        + "${name}-Installer.exe";
-    sha256 = "19a14av3bg6i4iy5q5pa737cwxznqji0lcrapxw0q6qb8rs1rhs7";
-  };
-
-  extensionId = "ilpoejcegjjlgpobjkpjmddkbdkdndaj";
-
-  buildInputs = [ p7zip jq ];
-
-  unpackCmd = "7z x -y -otavic-sign $curSrc";
-
-  phases = [ "unpackPhase" "patchPhase" "installPhase" ];
-
-  postPatch = ''
-    jq '.allowed_origins = [
-      "chrome-extension://'"$extensionId"'/"
-    ] | .path = "'"$out/share/libexec/travic-sign/travic-sign"'"
-      | del(.allowed_extensions)' manifest-firefox.json > host.json
-
-    7z x -y -oextension FirefoxExtension.xpi
-    jq '.content_scripts[].matches = ["https://karte.santanderbank.de/*"] | {
-      # All the object attributes that we want to have (nothing more):
-      background, web_accessible_resources, content_scripts, page_action,
-      permissions, author, version, description, name, manifest_version
-    }' extension/manifest.json > new_manifest.json
-    mv new_manifest.json extension/manifest.json
-    (cd extension && 7z a -tzip ../travic-sign.crx *)
-  '';
-
-  winePrefix = runCommand "empty-wineprefix" {
-    buildInputs = [ patchedWine ];
-  } ''
-    export WINEPREFIX="$out"
-    mkdir -p "$out"
-    wine wineboot.exe
-  '';
-
-  installPhase = ''
-    libexec="$out/share/libexec/travic-sign"
-
-    install -vD -m 0644 TRAVIC-Sign-Service.exe "$libexec/service.exe"
-    install -vD -m 0644 host.json \
-      "$out/etc/chromium/native-messaging-hosts/travic-sign.json"
-    install -vD -m 0644 travic-sign.crx \
-      "$out/share/chromium/extensions/$extensionId.crx"
-
-    cat > "$libexec/travic-sign" <<EOF
-    #!${stdenv.shell}
-    export WINEPREFIX="$winePrefix"
-    exec ${patchedWine}/bin/wine "$libexec/service.exe"
-    EOF
-    chmod +x "$libexec/travic-sign"
-  '';
-}
diff --git a/pkgs/aszlig/santander/pipelight.patch b/pkgs/aszlig/santander/pipelight.patch
deleted file mode 100644
index 3a07da72..00000000
--- a/pkgs/aszlig/santander/pipelight.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/src/windows/pluginloader/pluginloader.c b/src/windows/pluginloader/pluginloader.c
-index 9e8556f..c50be2a 100644
---- a/src/windows/pluginloader/pluginloader.c
-+++ b/src/windows/pluginloader/pluginloader.c
-@@ -1510,7 +1510,7 @@ void dispatcher(int functionid, Stack &stack){
- 				NPObject *objectValue;
- 				NPError result;
- 
--				if (variable == NPPVpluginScriptableNPObject)
-+				if (variable == NPPVpluginScriptableNPObject && pluginFuncs.getvalue)
- 					result = pluginFuncs.getvalue(instance, variable, &objectValue);
- 				else{
- 					DBG_WARN("FUNCTION_NPP_GETVALUE_OBJECT - variable %d not allowed", variable);
diff --git a/pkgs/aszlig/santander/winscard.patch b/pkgs/aszlig/santander/winscard.patch
deleted file mode 100644
index 7dfa04ac..00000000
--- a/pkgs/aszlig/santander/winscard.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/dlls/winscard/winscard.c	1970-01-01 01:00:01.000000000 +0100
-+++ b/dlls/winscard/winscard.c	2016-06-06 01:52:53.631444433 +0200
-@@ -1527,7 +1527,7 @@
- {

-     LONG lRet;

-     TRACE(" 0x%08X %p %p %p %p %p %p\n",(unsigned int) hCard,mszReaderNames,pcchReaderLen,pdwState,pdwProtocol,pbAtr,pcbAtrLen);

--    if(!pcchReaderLen || !pdwState || !pdwProtocol || !pcbAtrLen)

-+    if(!pcchReaderLen || !pcbAtrLen)

-         lRet = SCARD_E_INVALID_PARAMETER;

-     else if(!liteSCardStatus)

-         lRet = SCARD_F_INTERNAL_ERROR;

diff --git a/pkgs/aszlig/vim/default.nix b/pkgs/aszlig/vim/default.nix
index 6c1c6bab..5a2a3962 100644
--- a/pkgs/aszlig/vim/default.nix
+++ b/pkgs/aszlig/vim/default.nix
@@ -335,6 +335,27 @@ let
       '';
       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"
+      '';
+    };
   };
 
   generic = ''
@@ -384,6 +405,13 @@ let
     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 = ''
@@ -394,6 +422,7 @@ let
     " 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
@@ -415,8 +444,8 @@ let
     " prevent colorscheme from overriding these highlights
     au ColorScheme * highlight ExtraWhitespace ctermbg=red guibg=red
 
-    " highlight everything exceeding 79 characters (except for CSV)
-    au BufWinEnter * if &ft !=# 'csv'
+    " highlight everything exceeding 79 characters (with exceptions)
+    au BufWinEnter * if index(['csv', 'strace', 'xt'], &ft) < 0
       \ | let w:m2=matchadd('ErrorMsg', '\%>79v.\+', -1)
       \ | endif
 
diff --git a/pkgs/build-support/build-sandbox/default.nix b/pkgs/build-support/build-sandbox/default.nix
index 66797268..4e5cffe9 100644
--- a/pkgs/build-support/build-sandbox/default.nix
+++ b/pkgs/build-support/build-sandbox/default.nix
@@ -11,6 +11,10 @@ let
   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
@@ -26,11 +30,13 @@ in stdenv.mkDerivation ({
 
   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
 
@@ -52,25 +58,36 @@ in stdenv.mkDerivation ({
     echo '#include "setup.h"' > params.c
     echo 'bool setup_app_paths(void) {' >> params.c
 
-    for dep in $runtimeDeps; do
-      echo 'if (!bind_mount("'"$dep"'", true, true, true)) return false;' \
+    ${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
-    done
+
+    '' 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
 
-    echo 'bool mount_runtime_path_vars(struct query_state *qs) {' >> 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}
+      ${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
+      echo 'return true; }' >> params.c
+    ''}
   '';
 
   postInstall = ''
@@ -82,8 +99,9 @@ in stdenv.mkDerivation ({
   '';
 
   nativeBuildInputs = [ pkgconfig ];
-  buildInputs = [ nix boost ];
+  buildInputs = [ boost nix ];
   makeFlags = [ "BINDIR=${drv}/bin" ]
-           ++ lib.optional allowBinSh "BINSH_EXECUTABLE=${dash}/bin/dash";
+           ++ 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
index e18ec9d4..8e1218f6 100644
--- a/pkgs/build-support/build-sandbox/src/Makefile
+++ b/pkgs/build-support/build-sandbox/src/Makefile
@@ -1,19 +1,24 @@
 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
 
-OBJECTS = nix-query.o path-cache.o params.o setup.o
-
-CFLAGS = -g -Wall -std=gnu11 -DFS_ROOT_DIR=\"$(out)\"
 ifdef BINSH_EXECUTABLE
 CFLAGS += -DBINSH_EXECUTABLE=\"$(BINSH_EXECUTABLE)\"
 endif
-CXXFLAGS = -g -Wall -std=c++14 `pkg-config --cflags nix-main`
-CXXFLAGS += -DNIX_VERSION=$(NIX_VERSION)
-LDFLAGS = -Wl,--copy-dt-needed-entries `pkg-config --libs nix-main`
 
 all: $(OBJECTS)
 
diff --git a/pkgs/build-support/build-sandbox/src/setup.c b/pkgs/build-support/build-sandbox/src/setup.c
index feafd6f6..98205710 100644
--- a/pkgs/build-support/build-sandbox/src/setup.c
+++ b/pkgs/build-support/build-sandbox/src/setup.c
@@ -9,7 +9,6 @@
 #include <fcntl.h>
 #include <libgen.h>
 #include <limits.h>
-#include <malloc.h>
 #include <sched.h>
 #include <stdbool.h>
 #include <stdio.h>
@@ -18,8 +17,10 @@
 #include <unistd.h>
 
 #include "params.h"
-#include "nix-query.h"
 #include "path-cache.h"
+#ifndef FULL_NIX_STORE
+#include "nix-query.h"
+#endif
 
 static path_cache cached_paths = NULL;
 
@@ -604,6 +605,7 @@ static bool setup_binsh(const char *executable)
 }
 #endif
 
+#ifndef FULL_NIX_STORE
 static bool is_dir(const char *path)
 {
     struct stat sb;
@@ -663,6 +665,9 @@ bool mount_from_path_var(struct query_state *qs, const char *name)
     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];
@@ -680,6 +685,7 @@ static bool setup_static_etc(struct query_state *qs)
     return mount_requisites(qs, dest);
 }
 
+/* Bind-mount all necessary nix store paths. */
 static bool setup_runtime_paths(void)
 {
     struct query_state *qs;
@@ -702,6 +708,7 @@ static bool setup_runtime_paths(void)
     free_query(qs);
     return true;
 }
+#endif
 
 static bool setup_runtime_debug(void)
 {
@@ -782,8 +789,11 @@ static bool setup_chroot(void)
     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;
@@ -864,9 +874,25 @@ bool setup_sandbox(void)
     /* 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) {
-        waitpid(pid, NULL, 0);
-        _exit(1);
+
+        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();
diff --git a/pkgs/games/gog/crosscode.nix b/pkgs/games/gog/crosscode.nix
index 9e60ab08..f33cdb45 100644
--- a/pkgs/games/gog/crosscode.nix
+++ b/pkgs/games/gog/crosscode.nix
@@ -2,12 +2,12 @@
 
 buildGame rec {
   name = "crosscode-${version}";
-  version = "1.0.2";
+  version = "1.1.0";
 
   src = fetchGog {
     productId = 1252295864;
     downloadName = "en3installer0";
-    sha256 = "0gd3i99g79w7nr6dnkjkpfq5s2y20dwrf706ipzkggknygmg9xad";
+    sha256 = "1rqf1vlg151hxy5f9nwldmb4l3853dmvcf7fiakab8vzsmjmldlm";
   };
 
   nativeBuildInputs = [ makeWrapper ];
diff --git a/pkgs/games/gog/default.nix b/pkgs/games/gog/default.nix
index 1ef58c30..f1010a00 100644
--- a/pkgs/games/gog/default.nix
+++ b/pkgs/games/gog/default.nix
@@ -15,13 +15,17 @@ let
     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 {};
     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 {};
   };
diff --git a/pkgs/games/gog/fetch-gog/default.nix b/pkgs/games/gog/fetch-gog/default.nix
index b49f0dab..c1d350ce 100644
--- a/pkgs/games/gog/fetch-gog/default.nix
+++ b/pkgs/games/gog/fetch-gog/default.nix
@@ -147,13 +147,24 @@ let
       }
     '';
 
-  in runCommandCC "get-captcha" {
-    nativeBuildInputs = [ pkgconfig ];
+  in stdenv.mkDerivation {
+    name = "get-captcha";
+
+    dontUnpack = true;
+
+    nativeBuildInputs = [ pkgconfig (qt5.wrapQtAppsHook or null) ];
     buildInputs = [ qt5.qtbase qt5.qtwebengine ];
-  } ''
-    g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
-      -Wall -std=c++11 -o "$out" ${application}
-  '';
+    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)}'";
 
@@ -177,7 +188,7 @@ let
       def login(self):
         browser = mechanicalsoup.StatefulBrowser()
         response = browser.open(${mkPyStr authURL})
-        if "google.com/recaptcha" in response.text:
+        if "https://www.recaptcha.net/recaptcha" in response.text:
           token_url = self.login_with_captcha()
         else:
           browser.select_form('form[name="login"]')
@@ -185,13 +196,19 @@ let
           browser['login[password]'] = ${mkPyStr password}
           browser.submit_selected()
 
-          auth_code = parse_qs(urlsplit(browser.get_url()).query)['code']
+          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': auth_code,
+            'code': query['code'],
             'redirect_uri': ${mkPyStr redirectUri}
           })
 
@@ -203,7 +220,8 @@ let
 
       def login_with_captcha(self):
         sys.stderr.write("Solving a captcha is required to log in.\n")
-        sys.stderr.write("Please run " ${mkPyStr getCaptcha} " now.\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
@@ -278,6 +296,8 @@ in stdenv.mkDerivation {
   outputHashAlgo = "sha256";
   outputHash = sha256;
 
+  preferLocalBuild = true;
+
   nativeBuildInputs = [
     curl python3Packages.tabulate python3Packages.MechanicalSoup
   ];
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/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/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/humblebundle/default.nix b/pkgs/games/humblebundle/default.nix
index 2193ca50..5de3f40e 100644
--- a/pkgs/games/humblebundle/default.nix
+++ b/pkgs/games/humblebundle/default.nix
@@ -25,6 +25,7 @@ let
     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 {};
diff --git a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
index 063d4a02..1340bce8 100644
--- a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
+++ b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
@@ -1,13 +1,20 @@
-{ stdenv, curl, cacert, writeText, fetchFromGitHub, fetchpatch
-, python, pythonPackages
+{ stdenv, curl, cacert, writeText, writeScript, fetchFromGitHub, fetchpatch
+, python, python3, pythonPackages
 
 # Dependencies for the captcha solver
-, pkgconfig, qt5, runCommandCC
+, pkgconfig, qt5
 
 , email, password
 }:
 
-{ name ? null, machineName, downloadName ? "Download", suffix ? "humblebundle", md5 }: let
+{ name ? null
+, machineName
+, downloadName ? "Download"
+, suffix ? "humblebundle"
+, md5
+}:
+
+let
   cafile = "${cacert}/etc/ssl/certs/ca-bundle.crt";
 
   getCaptcha = let
@@ -77,12 +84,34 @@
       }
     '';
 
-  in runCommandCC "get-captcha" {
-    nativeBuildInputs = [ pkgconfig ];
+  in stdenv.mkDerivation {
+    name = "get-captcha";
+
+    dontUnpack = true;
+
+    nativeBuildInputs = [ pkgconfig (qt5.wrapQtAppsHook or null) ];
     buildInputs = [ qt5.qtbase qt5.qtwebengine ];
-  } ''
-    g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
-      -Wall -std=c++11 -o "$out" ${application}
+    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 {
@@ -96,6 +125,8 @@
       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' \
@@ -137,7 +168,8 @@
 
     def login_with_captcha(hb):
       print >>sys.stderr, "Solving a captcha is required to log in."
-      print >>sys.stderr, "Please run " ${pyStr (toString getCaptcha)} " now."
+      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
@@ -152,13 +184,41 @@
       response = sock.recv(4096)
       sock.close()
       print >>sys.stderr, "Captcha solved correctly, logging in."
-      hb.login(${pyStr email}, ${pyStr password}, recaptcha_response=response)
+      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()
-    try:
-      hb.login(${pyStr email}, ${pyStr password})
-    except humblebundle.exceptions.HumbleCaptchaException:
-      login_with_captcha(hb)
+    api_login(hb)
 
     products = dict(get_products(hb))
     dstruct = find_download(products)
@@ -183,6 +243,7 @@ in stdenv.mkDerivation {
   outputHashAlgo = "md5";
   outputHash = md5;
 
+  preferLocalBuild = true;
   buildInputs = [ python humbleAPI ];
 
   buildCommand = ''
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/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/itch/fetch-itch/default.nix b/pkgs/games/itch/fetch-itch/default.nix
index 3700f5c6..121868be 100644
--- a/pkgs/games/itch/fetch-itch/default.nix
+++ b/pkgs/games/itch/fetch-itch/default.nix
@@ -69,6 +69,7 @@ in stdenv.mkDerivation {
 
   SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
 
+  preferLocalBuild = true;
   nativeBuildInputs = [ python3Packages.python ];
 
   buildCommand = ''
diff --git a/pkgs/games/steam/fetchsteam/default.nix b/pkgs/games/steam/fetchsteam/default.nix
index 5c1faf55..646e0a14 100644
--- a/pkgs/games/steam/fetchsteam/default.nix
+++ b/pkgs/games/steam/fetchsteam/default.nix
@@ -79,6 +79,7 @@ let
 in with stdenv.lib; runCommand "${name}-src" {
   buildInputs = [ DepotDownloader ];
   inherit username password appId depotId manifestId;
+  preferLocalBuild = true;
   outputHashAlgo = "sha256";
   outputHash = sha256;
   outputHashMode = "recursive";
diff --git a/pkgs/profpatsch/default.nix b/pkgs/profpatsch/default.nix
index a4378ecf..dc746c2e 100644
--- a/pkgs/profpatsch/default.nix
+++ b/pkgs/profpatsch/default.nix
@@ -5,11 +5,25 @@ let
 
   # wrapper for execlineb that doesn’t need the execline commands
   # in PATH to work (making them appear like “builtins”)
+  # TODO: upstream into nixpkgs
+  # TODO: the grep could be nicer
   execlineb-with-builtins =
     let eldir = "${pkgs.execline}/bin";
     in pkgs.writeScriptBin "execlineb" ''
       #!${eldir}/execlineb -s0
+      # appends the execlineb bin dir to PATH if not yet in PATH
       ${eldir}/define eldir ${eldir}
+      ''${eldir}/ifelse
+      {
+        # since this is nix, we can grep for the execline drv hash in PATH
+        # to see whether it’s already in there
+        ''${eldir}/pipeline
+        { ${pkgs.coreutils}/bin/printenv PATH }
+        ${pkgs.gnugrep}/bin/grep --quiet "${eldir}"
+      }
+      # it’s there already
+      { ''${eldir}/execlineb $@ }
+      # not there yet, add it
       ''${eldir}/importas oldpath PATH
       ''${eldir}/export PATH "''${eldir}:''${oldpath}"
       ''${eldir}/execlineb $@
@@ -46,6 +60,52 @@ let
       allowSubstitutes = false;
     }) cmd;
 
+  testing = import ./testing {
+    inherit stdenv lib;
+    inherit (runExeclineFns) runExecline;
+    inherit (pkgs) runCommand;
+    bin = bins pkgs.s6PortableUtils [ "s6-touch" "s6-echo" ];
+  };
+
+  runExeclineFns =
+    # todo: factor out calling tests
+    let
+      it = import ./execline/run-execline.nix {
+        bin = (bins execlineb-with-builtins [ "execlineb" ])
+           // (bins pkgs.execline [ "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 = (bins execlineb-with-builtins [ "execlineb" ])
+           // (bins pkgs.execline [
+                 { use = "if"; as = "execlineIf"; }
+                 "redirfd" "importas"
+               ])
+           // (bins 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);
+    };
+
+
 in rec {
   inherit (nixperiments)
     # filterSource by parsing a .gitignore file
@@ -60,10 +120,11 @@ in rec {
     json2json json2string;
 
   backlight = callPackage ./backlight { inherit (pkgs.xorg) xbacklight; };
-  display-infos = callPackage ./display-infos {};
+  display-infos = callPackage ./display-infos { inherit sfttime; };
   git-commit-index = callPackage ./git-commit-index { inherit script runCommandLocal; };
   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;
@@ -90,35 +151,8 @@ in rec {
     ];
   });
 
-  runExecline =
-    # todo: factor out calling tests
-    let
-      it = import ./execline/run-execline.nix {
-        bin = (bins execlineb-with-builtins [ "execlineb" ])
-           // (bins pkgs.execline [ "redirfd" "importas" "exec" ]);
-        inherit stdenv;
-      };
-      tests = import ./execline/run-execline-tests.nix {
-        runExecline = it;
-        inherit (testing) drvSeqL;
-        inherit (pkgs) coreutils;
-        inherit stdenv;
-        bin = (bins execlineb-with-builtins [ "execlineb" ])
-           // (bins pkgs.execline [
-                 { use = "if"; as = "execlineIf"; }
-                 "redirfd" "importas"
-               ])
-           // (bins pkgs.s6PortableUtils
-                [ "s6-cat" "s6-grep" "s6-touch" "s6-test" "s6-chmod" ]);
-       };
-    in tests;
-
-
-  testing = import ./testing {
-    inherit stdenv lib runExecline;
-    inherit (pkgs) runCommand;
-    bin = bins pkgs.s6PortableUtils [ "s6-touch" "s6-echo" ];
-  };
+  inherit (runExeclineFns)
+    runExecline runExeclineLocal;
 
   symlink = pkgs.callPackage ./execline/symlink.nix {
     inherit runExecline;
diff --git a/pkgs/profpatsch/display-infos/default.nix b/pkgs/profpatsch/display-infos/default.nix
index d213241c..b626e844 100644
--- a/pkgs/profpatsch/display-infos/default.nix
+++ b/pkgs/profpatsch/display-infos/default.nix
@@ -1,8 +1,8 @@
-{ lib, runCommand, python3, libnotify }:
+{ lib, runCommand, writeText, python3, libnotify, bc, sfttime }:
 
 let
   name = "display-infos-0.1.0";
-  script = builtins.toFile (name + "-script") ''
+  script = writeText (name + "-script") ''
     #!@python3@
 
     import sys
@@ -11,20 +11,53 @@ let
     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*"):
-        def readint(fn):
-            with open(fn, 'r') as f:
-                return int(f.read())
 
+        # 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()
-    notify = "BAT: {}% | {}".format(int(bat*100), date)
-    sub.run(["@notify-send@", notify])
+    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
@@ -33,6 +66,7 @@ in
   } ''
     substitute ${script} script \
       --replace "@python3@" "${getBin python3}/bin/python3" \
-      --replace "@notify-send@" "${getBin libnotify}/bin/notify-send"
+      --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/escape.nix b/pkgs/profpatsch/execline/escape.nix
new file mode 100644
index 00000000..d9a0be0c
--- /dev/null
+++ b/pkgs/profpatsch/execline/escape.nix
@@ -0,0 +1,30 @@
+{ 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 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/run-execline-tests.nix b/pkgs/profpatsch/execline/run-execline-tests.nix
index ebfdeb20..c3f534cc 100644
--- a/pkgs/profpatsch/execline/run-execline-tests.nix
+++ b/pkgs/profpatsch/execline/run-execline-tests.nix
@@ -2,26 +2,24 @@
 # https://www.mail-archive.com/skaware@list.skarnet.org/msg01256.html
 , coreutils }:
 
-# TODO: run all of these locally! runExeclineLocal
-
 let
 
   # lol
-  writeScript = name: script: runExecline {
-    inherit name;
+  writeScript = name: script: runExecline name {
     derivationArgs = {
       inherit script;
       passAsFile = [ "script" ];
+      preferLocalBuild = true;
+      allowSubstitutes = false;
     };
-    execline = ''
-      importas -ui s scriptPath
-      importas -ui out out
-      foreground {
-        ${coreutils}/bin/mv $s $out
-      }
-      ${bin.s6-chmod} 0755 $out
-    '';
-   };
+  } [
+      "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 ++ [ "" ];
@@ -30,7 +28,7 @@ let
   # in the given file. Does not use runExecline, because
   # that should be tested after all.
   fileHasLine = line: file: derivation {
-    name = "file-${file.name}-has-line";
+    name = "run-execline-test-file-${file.name}-has-line";
     inherit (stdenv) system;
     builder = bin.execlineIf;
     args =
@@ -43,41 +41,49 @@ let
         bin.importas "-ui" "out" "out"
         bin.s6-touch "$out"
       ];
+    preferLocalBuild = true;
+    allowSubstitutes = false;
   };
 
   # basic test that touches out
-  basic = runExecline {
-    name = "basic";
-    execline = ''
-      importas -ui out out
-      ${bin.s6-touch} $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 {
-    name = "stdin";
+  stdin = fileHasLine "foo" (runExecline "run-execline-test-stdin" {
     stdin = "foo\nbar\nfoo";
-    execline = ''
-      importas -ui out out
+    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}
-    '';
-  });
+      "redirfd" "-w" "1" "$out" bin.s6-cat
+  ]);
 
-  wrapWithVar = runExecline {
-    name = "wrap-with-var";
+  wrapWithVar = runExecline "run-execline-test-wrap-with-var" {
     builderWrapper = writeScript "var-wrapper" ''
       #!${bin.execlineb} -S0
       export myvar myvalue $@
     '';
-    execline = ''
-      importas -ui v myvar
-      if { ${bin.s6-test} myvalue = $v }
-        importas out out
-        ${bin.s6-touch} $out
-    '';
-  };
+    derivationArgs = {
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+    };
+  } [
+    "importas" "-ui" "v" "myvar"
+    "if" [ bin.s6-test "myvalue" "=" "$v" ]
+      "importas" "out" "out"
+      bin.s6-touch "$out"
+  ];
 
-in args: drvSeqL [ basic stdin wrapWithVar ] (runExecline args)
+in [ basic stdin wrapWithVar ]
diff --git a/pkgs/profpatsch/execline/run-execline.nix b/pkgs/profpatsch/execline/run-execline.nix
index dbc6f4fd..2efe43d6 100644
--- a/pkgs/profpatsch/execline/run-execline.nix
+++ b/pkgs/profpatsch/execline/run-execline.nix
@@ -1,15 +1,18 @@
-{ stdenv, bin }:
-{ name
-# the execline script as string
-, execline
+{ stdenv, bin, lib }:
+name:
+{
 # a string to pass as stdin to the execline script
-, stdin ? ""
+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;
@@ -18,6 +21,7 @@ assert !derivationArgs ? builder;
 assert !derivationArgs ? args;
 
 derivation (derivationArgs // {
+  # TODO: what about cross?
   inherit (stdenv) system;
   inherit name;
 
@@ -26,7 +30,10 @@ derivation (derivationArgs // {
   # to pass the script and stdin as envvar;
   # this might clash with another passed envar,
   # so we give it a long & unique name
-  _runExeclineScript = execline;
+  _runExeclineScript =
+    let
+      escape = (import ./escape.nix { inherit lib; });
+    in escape.escapeExecline execline;
   _runExeclineStdin = stdin;
   passAsFile = [
     "_runExeclineScript"
diff --git a/pkgs/profpatsch/execline/symlink.nix b/pkgs/profpatsch/execline/symlink.nix
index ca8684d2..c6a311d8 100644
--- a/pkgs/profpatsch/execline/symlink.nix
+++ b/pkgs/profpatsch/execline/symlink.nix
@@ -11,9 +11,7 @@ let
     "${toString (builtins.stringLength s)}:${s},";
 
 in
-runExecline {
-  inherit name;
-
+runExecline name {
   derivationArgs = {
     pathTuples = lib.concatMapStrings
       ({dest, orig}: toNetstring
@@ -23,28 +21,26 @@ runExecline {
     # 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}''
 
-  execline = ''
-    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
-      }
+    # 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
-      }
-  '';
-}
+    "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/nman/default.nix b/pkgs/profpatsch/nman/default.nix
index b9b967cf..bbf4f00f 100644
--- a/pkgs/profpatsch/nman/default.nix
+++ b/pkgs/profpatsch/nman/default.nix
@@ -6,7 +6,9 @@ runCommandNoCC "nman" {
     license = licenses.gpl3;
   };
 } ''
-    ${lib.getBin go}/bin/go build -o nman ${./nman.go}
+    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/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/utils-hs/default.nix b/pkgs/profpatsch/utils-hs/default.nix
index 423feb03..9ddd50ec 100644
--- a/pkgs/profpatsch/utils-hs/default.nix
+++ b/pkgs/profpatsch/utils-hs/default.nix
@@ -53,11 +53,23 @@ let
             rev = "e7efbb4f0624e86109acd818942c8cd18a7d9d3d";
             sha256 = "0dismb9vl5fxynasc2kv5baqyzp6gpyybmd5p9g1hlcq3p7pfi24";
           };
+          broken = false;
           buildDepends = old.buildDepends or [] ++ (with hself; [
             dependent-sum prettyprinter (hlib.doJailbreak ref-tf)
           ]);
         });
-      } );# // (import /home/philip/kot/dhall/overlay.nix { inherit haskell fetchFromGitHub; } hself hsuper));
+
+        dhall-nix = hlib.justStaticExecutables (hlib.overrideCabal hsuper.dhall-nix (old: {
+          src = fetchFromGitHub {
+            owner = "Profpatsch";
+            repo = "dhall-nix";
+            # manual update to dhall @0.19
+            rev = "feae0ce5b2ecf4daeeae15c39f427f126c33da7c";
+            sha256 = "1kdsbnj681lf65dsdclcrzj4cab1hh0v22n2140386zvwmawyp6r";
+          };
+          broken = false;
+        }));
+      });
     };
 
   haskellDrv = { name, subfolder, deps }: hps.mkDerivation {
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"