Add utils.readFile and utils.interpolateFile, use them in xmonad config

- Add utils.readFile which reads a file and evaluates any nix in ${{null}}
  brackets like that, returning a string. This should be a drop-in replacement
  for builtins.readFile
- Add utils.interpolateFile which does the same as readFile but also writes to
  the store and returns a store path for the new file. This should be a drop-in
  wrapper for a path
- Use utils.interpolateFile on the xmonad config file
- Use a relative file path contained in nix interpolation brackets in the xmonad
  config file
- Use package paths inside the included file (all included paths are also
  interpolated)
- Update xmonad to have better volume change support
- Update xmonad to have better multi-display support (actually start polybar,
  display different colors for second displays)
diff --git a/flake.nix b/flake.nix
index 8ee4f1c..6672843 100644
--- a/flake.nix
+++ b/flake.nix
@@ -68,7 +68,7 @@
           });
         };
 
-        utils = import ./utils nixpkgs.lib;
+        utils = import ./utils pkgs;
 
         username = "minion";
 
diff --git a/modules/boot.nix b/modules/boot.nix
index f93c554..8a4a8af 100644
--- a/modules/boot.nix
+++ b/modules/boot.nix
@@ -1,6 +1,11 @@
 { pkgs, options, lib, ... }: {
   config = {
+    services.logind.extraConfig = ''
+      HandlePowerKey=Ignore
+      LidSwitchIgnoreInhibited=no
+    '';
     boot = {
+      kernelParams = [ "acpi_backlight=video" ];
       loader = {
         systemd-boot = {
           /* enable = true; */ # Replaced by secure-boot.nix
@@ -24,6 +29,7 @@
         "tap"
         "tun"
         "veth"
+        "hid_sensor_hub"
       ];
     };
   };
diff --git a/modules/xmonad.nix b/modules/xmonad.nix
index 0457b73..9195235 100644
--- a/modules/xmonad.nix
+++ b/modules/xmonad.nix
@@ -1,17 +1,18 @@
-{ lib, pkgs, home, config, ... }: {
+{ lib, pkgs, home, config, utils, ... }: {
   home = {
+    home.packages = with pkgs; [ xob pamixer ];
     xsession = {
       windowManager.xmonad = {
         enable = true;
         enableContribAndExtras = true;
-        config = ./xmonad/xmonad.hs;
+        config = utils.interpolateFile ./xmonad/xmonad.hs;
         libFiles = lib.pipe ./xmonad [
           builtins.readDir
           builtins.attrNames
           (builtins.filter (name: name != "xmonad.hs"))
           (map (name: {
             inherit name;
-            value = "${./xmonad}/${name}";
+            value = utils.interpolateFile "${./xmonad}/${name}";
           }))
           builtins.listToAttrs
         ];
diff --git a/modules/xmonad/XMonadLog.hs b/modules/xmonad/XMonadLog.hs
index b07485c..fb5bbe7 100644
--- a/modules/xmonad/XMonadLog.hs
+++ b/modules/xmonad/XMonadLog.hs
@@ -33,11 +33,12 @@
       grey   = "#474e5d"
       orange = "#e5c07b"
       purple = "#c678dd"
+      green  = "#98c379"
       red    = "#e06c75"
   in def { ppOutput          = dbusOutput dbus
          , ppCurrent         = wrapper red
-         , ppVisible         = wrapper blue
-         , ppUrgent          = wrapper orange
+         , ppVisible         = wrapper orange
+         , ppUrgent          = wrapper green
          , ppHidden          = wrapper blue
          , ppHiddenNoWindows = wrapper grey
          , ppTitle           = shorten 100 . wrapper purple
diff --git a/modules/xmonad/vol_change.py b/modules/xmonad/vol_change.py
new file mode 100644
index 0000000..6629995
--- /dev/null
+++ b/modules/xmonad/vol_change.py
@@ -0,0 +1,30 @@
+#!${{pkgs.python3}}/bin/python3
+
+import sys
+import os
+import subprocess
+
+args = sys.argv
+
+device_argument = "--default-source" if "-i" in args else ""
+
+control_argument = ""
+control_argument += "-i" if "-u" in args else ""
+control_argument += "-d" if "-d" in args else ""
+control_argument += "-t" if "-m" in args else " 5"
+
+os.system(
+    f"${{pkgs.pamixer}}/bin/pamixer {device_argument} {control_argument} --set-limit 150"
+)
+mute_char = (
+    "!"
+    if subprocess.getoutput(f"${{pkgs.pamixer}}/bin/pamixer {device_argument} --get-mute")
+    == "true"
+    else ""
+)
+volume = subprocess.getoutput(f"${{pkgs.pamixer}}/bin/pamixer {device_argument} --get-volume")
+
+socket_path = f"{os.environ['XDG_RUNTIME_DIR']}/xob.sock"
+
+with open(socket_path, "w") as socket:
+    socket.write(f"{volume}{mute_char}\n")
diff --git a/modules/xmonad/xmonad.hs b/modules/xmonad/xmonad.hs
index b1b3219..76c29f5 100644
--- a/modules/xmonad/xmonad.hs
+++ b/modules/xmonad/xmonad.hs
@@ -1,4 +1,5 @@
 -- spell-checker:words xmonad
+{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-}
 import           XMonad
 
 import           System.Exit
@@ -14,20 +15,28 @@
 
 import           Blaze.ByteString.Builder     (toByteString)
 import           Foreign.C
-import           XMonad
+import           Graphics.X11.ExtraTypes      (xF86XK_AudioLowerVolume,
+                                               xF86XK_AudioMute,
+                                               xF86XK_AudioRaiseVolume,
+                                               xF86XK_MonBrightnessDown,
+                                               xF86XK_MonBrightnessUp,
+                                               xF86XK_Xfer)
+import qualified XMonad                       as W
 import           XMonad.Hooks.DynamicProperty (dynamicPropertyChange)
 import           XMonad.Hooks.ManageHelpers   (doFullFloat, doLower,
-                                               isInProperty, doRectFloat)
+                                               doRectFloat, isInProperty)
 import           XMonad.Hooks.UrgencyHook
 import           XMonad.Layout.Drawer         (propertyToQuery)
 import           XMonad.Layout.Gaps
 import           XMonad.Layout.Spacing
 import qualified XMonad.StackSet              as W
 import           XMonad.Util.Hacks
-import qualified XMonad as W
-import XMonad.Util.PureX (curScreenId, curScreen)
+import           XMonad.Util.PureX            (curScreen, curScreenId)
+import           XMonad.Util.Run              (safeSpawn, safeSpawnProg)
 
-terminal = "kitty"      -- Kitty, my beloved <3
+volumeChangeCmd = "${{./vol_change.py}}"
+
+terminal = "/usr/bin/env SHLVL=0 kitty"      -- Kitty, my beloved <3
 launcher = "pkill rofi; rofi -show combi"
 networkManager = "wpa_cli select_network $(wpa_cli list_networks | tail -n +3 | awk '!seen[$2]++' | rofi -dmenu -window-title 'Select Network' | awk '{print $1;}')"
 
@@ -36,7 +45,7 @@
 
 modifierKey = mod4Mask  -- Use Super as our mod key
 
-statusBar = "pkill polybar; polybar"
+statusBar = "pkill polybar; polybar main; polybar dp1; polybar dp2; polybar dp3; polybar dp4"
 compositor = "pkill picom; picom"
 background = "pkill show; show ~/.xmonad/wallpaper.glsl > /dev/null"
 colorSelection = "xcolor | xclip -sel clip"
@@ -44,11 +53,16 @@
 
 shift = shiftMask
 
+-- spell-checker:words xobsock
+xobsock = "$XDG_RUNTIME_DIR/xob.sock"
+
 startupHook = do
   spawn Main.statusBar
   spawn Main.compositor
   spawn background
   spawn keybindings
+  spawn "pgrep keepass || run_keepass"
+  spawn $ "pkill xob; rm -f " ++ xobsock ++ " && mkfifo " ++ xobsock ++ " && tail -f " ++ xobsock ++ " | xob"
 
 
 main :: IO ()
@@ -92,4 +106,15 @@
   , ((modifierKey .|. Main.shift, xK_s), spawn selectScreenshot)
   , ((modifierKey .|. Main.shift, xK_h), spawn colorSelection)
   , ((0, xK_Print), spawn screenshot)
+  , ((modifierKey .|. Main.shift, xK_Return), spawn Main.terminal)
+  , ((0, xF86XK_AudioLowerVolume), spawn $ volumeChangeCmd ++ " -d")
+  , ((0, xF86XK_AudioRaiseVolume), spawn $ volumeChangeCmd ++ " -u")
+  , ((0, xF86XK_AudioMute), spawn $ volumeChangeCmd ++ " -m")
+  , ((modifierKey, xF86XK_AudioLowerVolume), spawn $ volumeChangeCmd ++ " -d -i")
+  , ((modifierKey, xF86XK_AudioRaiseVolume), spawn $ volumeChangeCmd ++ " -u -i")
+  , ((modifierKey, xF86XK_AudioMute), spawn $ volumeChangeCmd ++ " -m -i")
+  , ((0, xF86XK_MonBrightnessDown), spawn $ "light -U 6 && light -G | cut -d'.' -f1 > " ++ xobsock)
+  , ((0, xF86XK_MonBrightnessUp), spawn $ "light -A 6 && light -G | cut -d'.' -f1 > " ++ xobsock)
+  , ((modifierKey, xF86XK_MonBrightnessDown), spawn $ "light -U 3 && light -G | cut -d'.' -f1 > " ++ xobsock)
+  , ((modifierKey, xF86XK_MonBrightnessUp), spawn $ "light -A 3 && light -G | cut -d'.' -f1 > " ++ xobsock)
   ]
diff --git a/utils/default.nix b/utils/default.nix
index eb80965..4be13b9 100644
--- a/utils/default.nix
+++ b/utils/default.nix
@@ -1,12 +1,27 @@
-lib:
-lib.pipe ./. [
-  (import ./nixFilesInWithName.nix lib)
-  (builtins.map ({ name
-                 , path
-                 ,
-                 }: {
-    name = lib.removeSuffix ".nix" name;
-    value = import path lib;
-  }))
-  builtins.listToAttrs
-]
+pkgs_or_lib:
+let
+  is_pkgs = pkgs_or_lib ? lib;
+  lib = if is_pkgs then pkgs_or_lib.lib else pkgs_or_lib;
+  utils = lib.pipe ./. [
+    (import ./nixFilesInWithName.nix lib)
+    (builtins.map (file: rec {
+      name = lib.removeSuffix ".nix" file.name;
+      func = import file.path;
+      accepts_pkgs = builtins.hasAttr "pkgs" (builtins.functionArgs func);
+      value =
+        if accepts_pkgs then
+          func
+            (builtins.intersectAttrs (builtins.functionArgs func) {
+              inherit
+                lib utils; pkgs = pkgs_or_lib;
+            })
+        else if is_pkgs
+        then func lib
+        else
+          func pkgs_or_lib;
+      include = file.name != "default.nix" && (!accepts_pkgs || is_pkgs);
+    }))
+    (builtins.filter (utility: utility.include))
+    builtins.listToAttrs
+  ];
+in utils
diff --git a/utils/interpolateFile.nix b/utils/interpolateFile.nix
new file mode 100644
index 0000000..3665096
--- /dev/null
+++ b/utils/interpolateFile.nix
@@ -0,0 +1,8 @@
+{ utils, pkgs }: file: pkgs.lib.pipe file [
+  utils.readFile
+  (text: pkgs.writeTextFile {
+    name = builtins.baseNameOf file;
+    inherit text;
+    executable = true; # TODO: write and use utils.isExecutable
+  })
+]
diff --git a/utils/readFile.nix b/utils/readFile.nix
new file mode 100644
index 0000000..186b7a0
--- /dev/null
+++ b/utils/readFile.nix
@@ -0,0 +1,42 @@
+{ pkgs, utils }:
+let
+  lib = pkgs.lib;
+in
+file: lib.pipe file [
+  builtins.readFile
+  (builtins.split "\\\$\\{\\{([^}]*)}}")
+  (map (part:
+    if builtins.typeOf part == "string"
+    then part
+    else
+      import
+        (
+          (
+            pkgs.writeText "generated.nix" ("pkgs: lib: " + (builtins.elemAt part 0))
+          ).outPath
+        )
+        pkgs
+        lib
+  ))
+  (map (part:
+    if builtins.typeOf part == "string" then part
+    else if builtins.typeOf part == "path" then
+      let
+        stringified = toString part;
+      in
+      builtins.toString (utils.interpolateFile (
+        file + "/.." + (builtins.substring
+          (builtins.stringLength "/nix/store")
+          (builtins.stringLength
+            stringified)
+          stringified)
+        # ^ Somewhat of a hack, works because we know that the file path
+        # is a text file (i.e. not a directory) as we read it earlier and
+        # relative paths end up in /nix/store due to the writeText. Absolute
+        # paths are not supported
+      ))
+    else
+      toString part
+  ))
+  (builtins.concatStringsSep "")
+]