about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--modules/module-list.nix1
-rw-r--r--modules/programs/foot/default.nix204
2 files changed, 205 insertions, 0 deletions
diff --git a/modules/module-list.nix b/modules/module-list.nix
index a964e56a..11b6d008 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -9,6 +9,7 @@
   ./hardware/thinkpad.nix
   ./programs/gnupg
   ./programs/fish/fasd.nix
+  ./programs/foot/default.nix
   ./services/drawpile.nix
   ./services/postfix
   ./services/starbound.nix
diff --git a/modules/programs/foot/default.nix b/modules/programs/foot/default.nix
new file mode 100644
index 00000000..cec2dce6
--- /dev/null
+++ b/modules/programs/foot/default.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (pkgs.vuizvui.sternenseemann.lib)
+    mapAttrsByAttrs
+    ;
+
+  inherit (lib.generators)
+    toINI
+    toKeyValue
+    ;
+
+  cfg = config.vuizvui.programs.foot;
+
+  # We don't allow null, since we use null as
+  # a “fall back to foot's defaults” value for defined
+  # options in the freeform module so no null may be
+  # present in the resulting ini file.
+  iniAtom = with lib.types; (oneOf [
+    bool
+    int
+    float
+    str
+  ]) // {
+    description = "INI atom (bool, int, float or string)";
+  };
+
+  # pkgs.formats.ini doesn't allow top-level bindings
+  # without a section, so we have to wrap it a bit
+  # TODO(sterni): multiple binds
+  format = {
+    type = with lib.types;
+      (attrsOf (either iniAtom (attrsOf iniAtom))) // {
+        description = ''
+          attribute set of either top-level INI atoms (bool, int, float or string) or attribute sets (sections) of INI atoms
+        '';
+      };
+    generate = name: value:
+      let
+        isSection = builtins.isAttrs;
+        topLevel = lib.filterAttrs (_: v: !(isSection v)) value;
+        sections = lib.filterAttrs (_: v: isSection v) value;
+      in
+        pkgs.writeText name ''
+          ${toKeyValue {} topLevel}
+          ${toINI {} sections}
+        '';
+  };
+
+  prettyPrint = lib.generators.toPretty {};
+
+  fontOptions = [
+    "font"
+    "font-bold"
+    "font-italic"
+    "font-bold-italic"
+  ];
+
+  exampleFontSet = {
+    font = "Dina";
+    options = {
+      slant = "italic";
+      weight = "bold";
+    };
+  };
+
+  # check if the given set is formed like we expect it to be
+  # and print a nice error message if not.
+  wellFormedFontSet = set:
+    let
+      attrCount = builtins.length (builtins.attrNames set);
+    in
+      lib.assertMsg (set ? font && attrCount <= 2
+          && (attrCount > 1 -> set ? options))
+        "font set must be of the form ${prettyPrint exampleFontSet} where the options attr is optional.";
+
+  # Convert a mixed list of font sets and strings into a comma
+  # separated string list of properly rendered fontconfig font strings
+  buildIniFontList = fonts:
+    let
+      formatOptions = opts: lib.concatStrings
+        (lib.mapAttrsToList (o: v: ":${o}=${builtins.toString v}") opts);
+      fontconfigFont = font:
+        if builtins.isString font
+        then font
+        else assert (wellFormedFontSet font);
+          "${font.font}${formatOptions (font.options or {})}";
+    in
+      lib.concatMapStringsSep "," fontconfigFont fonts;
+
+  mkFontOption = name: lib.mkOption {
+    type = with lib.types; nullOr (nonEmptyListOf (either str attrs));
+    description = ''
+      The first font specified is used as foot's
+      primary ${name}, all further fonts are
+      used as fallbacks in the specified order.
+      Fonts must be either specified as strings
+      in fontconfig syntax or using a special
+      record syntax (see example).
+    '';
+    example = lib.literalExample (prettyPrint [
+      {
+        font = "Courier New";
+        options = {
+          size = 12;
+        };
+      }
+      exampleFontSet
+      "monospace"
+    ]);
+    default = null;
+  };
+
+  commandBindOptions = [
+    "pipe-visible"
+    "pipe-scrollback"
+    "pipe-selected"
+  ];
+
+  exampleCommandBindSet = {
+    cmd = "sh -c 'xurls | bemenu | xargs -r $BROWSER'";
+    bind = "Control+Print";
+  };
+
+  wellformedCommandBindSet = set:
+    lib.assertMsg (set ? cmd && set ? bind)
+      "command bind set must contain a cmd and a bind attr: ${prettyPrint exampleCommandBindSet}";
+
+  buildIniCommandBind = bind:
+    if builtins.isString bind
+    then bind
+    else assert wellformedCommandBindSet bind;
+      "[${bind.cmd}] ${bind.bind}";
+
+  mkCommandBindOption = name:
+    lib.mkOption {
+      type = with lib.types; nullOr (either str (attrsOf str));
+      description = ''
+        Bind a key which pipes the ${lib.removePrefix "pipe-" name}
+        region into the given command.
+      '';
+      example = lib.literalExample (prettyPrint exampleCommandBindSet);
+      default = null;
+    };
+
+  # convert some fancy options we support to a format formats.ini can deal
+  # with and remove all optional options (in this case options which default
+  # to null), so we don't have to track upstreams defaults, but foot can
+  # decide for itself while we still can treat some options specially.
+  iniReady = settings:
+    let
+      withoutNulls =
+        lib.filterAttrsRecursive (_: x: x != null) settings;
+      attrTransforms =
+        (lib.genAttrs fontOptions (n: (_: buildIniFontList))) // {
+          key-bindings =
+            lib.genAttrs commandBindOptions (n: (_: buildIniCommandBind));
+        };
+    in
+      mapAttrsByAttrs withoutNulls attrTransforms;
+
+in {
+  options.vuizvui.programs.foot = {
+    enable = lib.mkEnableOption "foot";
+
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = format.type;
+        options = (lib.genAttrs fontOptions mkFontOption) // {
+          key-bindings =
+            lib.genAttrs commandBindOptions mkCommandBindOption;
+        };
+      };
+      default = {};
+      description = ''
+        Configuration for foot. A list of all available
+        options can be found in
+        <citerefentry>
+          <refentrytitle>foot</refentrytitle>
+          <manvolnum>5</manvolnum>
+        </citerefentry>
+        or at <link xlink:href="https://codeberg.org/dnkl/foot/src/tag/${pkgs.foot.version}/foot.ini" />.
+      '';
+      example = lib.literalExample (prettyPrint {
+        key-bindings = {
+          scrollback-up-page = "Control+Shift+Page_Up";
+          scrollback-down-page = "Control+Shift+Page_Down";
+          search-start = "Control+Shift+F";
+        };
+
+        mouse-bindings = {
+          primary-paste = "none";
+        };
+      });
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.foot ];
+
+    environment.etc."xdg/foot/foot.ini".source =
+      format.generate "foot.ini" (iniReady cfg.settings);
+  };
+}