From 41147ae09a125c80aaee2c047bf134b5384f0abe Mon Sep 17 00:00:00 2001
From: Olmo Kramer <olmo@codegrade.com>
Date: Sun, 27 Sep 2020 14:20:19 +0200
Subject: [PATCH] feh: allow binding actions to multiple buttons/keys

In feh you can bind multiple keys to the same action, but Home Manager
only let you set a single key to an action. You can cheat and pass a
string with space-separated keys, but with this change you can pass a
list for each action to bind multiple keys to it.

Also adds a couple of tests.

Fixes #1366
---
 modules/programs/feh.nix                      | 47 ++++++++++++-------
 tests/default.nix                             |  1 +
 tests/modules/programs/feh/default.nix        |  4 ++
 .../feh/feh-bindings-expected-buttons         |  4 ++
 .../programs/feh/feh-bindings-expected-keys   |  3 ++
 tests/modules/programs/feh/feh-bindings.nix   | 33 +++++++++++++
 .../programs/feh/feh-empty-settings.nix       | 15 ++++++
 7 files changed, 89 insertions(+), 18 deletions(-)
 create mode 100644 tests/modules/programs/feh/default.nix
 create mode 100644 tests/modules/programs/feh/feh-bindings-expected-buttons
 create mode 100644 tests/modules/programs/feh/feh-bindings-expected-keys
 create mode 100644 tests/modules/programs/feh/feh-bindings.nix
 create mode 100644 tests/modules/programs/feh/feh-empty-settings.nix

diff --git a/modules/programs/feh.nix b/modules/programs/feh.nix
index b1b33697e..e098342b5 100644
--- a/modules/programs/feh.nix
+++ b/modules/programs/feh.nix
@@ -6,8 +6,22 @@ let
 
   cfg = config.programs.feh;
 
-  disableBinding = func: key: func;
-  enableBinding = func: key: "${func} ${toString key}";
+  bindingsOf = t: with types; attrsOf (nullOr (either t (listOf t)));
+
+  renderBindings = bindings:
+    let
+      enabled = filterAttrs (n: v: v != null) bindings;
+      disabled = filterAttrs (n: v: v == null) bindings;
+      render = mapAttrsToList renderBinding;
+    in concatStringsSep "\n" (render disabled ++ render enabled);
+
+  renderBinding = func: key:
+    if key == null then
+      func
+    else if isList key then
+      concatStringsSep " " ([ func ] ++ map toString key)
+    else
+      "${func} ${toString key}";
 
 in {
   options.programs.feh = {
@@ -15,14 +29,16 @@ in {
 
     buttons = mkOption {
       default = { };
-      type = with types; attrsOf (nullOr (either str int));
+      type = with types; bindingsOf (either str int);
       example = {
         zoom_in = 4;
         zoom_out = "C-4";
+        prev_img = [ 3 "C-3" ];
       };
       description = ''
         Override feh's default mouse button mapping. If you want to disable an
-        action, set its value to null.
+        action, set its value to null. If you want to bind multiple buttons to
+        an action, set its value to a list.
         See <link xlink:href="https://man.finalrewind.org/1/feh/#x425554544f4e53"/> for
         default bindings and available commands.
       '';
@@ -30,14 +46,16 @@ in {
 
     keybindings = mkOption {
       default = { };
-      type = types.attrsOf (types.nullOr types.str);
+      type = bindingsOf types.str;
       example = {
         zoom_in = "plus";
         zoom_out = "minus";
+        prev_img = [ "h" "Left" ];
       };
       description = ''
         Override feh's default keybindings. If you want to disable a keybinding
-        set its value to null.
+        set its value to null. If you want to bind multiple keys to an action,
+        set its value to a list.
         See <link xlink:href="https://man.finalrewind.org/1/feh/#x4b455953"/> for
         default bindings and available commands.
       '';
@@ -53,18 +71,11 @@ in {
 
     home.packages = [ pkgs.feh ];
 
-    xdg.configFile."feh/buttons".text = ''
-      ${concatStringsSep "\n" (mapAttrsToList disableBinding
-        (filterAttrs (n: v: v == null) cfg.buttons))}
-      ${concatStringsSep "\n" (mapAttrsToList enableBinding
-        (filterAttrs (n: v: v != null) cfg.buttons))}
-    '';
+    xdg.configFile."feh/buttons" =
+      mkIf (cfg.buttons != { }) { text = renderBindings cfg.buttons + "\n"; };
 
-    xdg.configFile."feh/keys".text = ''
-      ${concatStringsSep "\n" (mapAttrsToList disableBinding
-        (filterAttrs (n: v: v == null) cfg.keybindings))}
-      ${concatStringsSep "\n" (mapAttrsToList enableBinding
-        (filterAttrs (n: v: v != null) cfg.keybindings))}
-    '';
+    xdg.configFile."feh/keys" = mkIf (cfg.keybindings != { }) {
+      text = renderBindings cfg.keybindings + "\n";
+    };
   };
 }
diff --git a/tests/default.nix b/tests/default.nix
index 7db9b16c0..acb115f8e 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -45,6 +45,7 @@ import nmt {
     ./modules/programs/browserpass
     ./modules/programs/dircolors
     ./modules/programs/direnv
+    ./modules/programs/feh
     ./modules/programs/fish
     ./modules/programs/git
     ./modules/programs/gpg
diff --git a/tests/modules/programs/feh/default.nix b/tests/modules/programs/feh/default.nix
new file mode 100644
index 000000000..48bab8ab6
--- /dev/null
+++ b/tests/modules/programs/feh/default.nix
@@ -0,0 +1,4 @@
+{
+  feh-empty-config = ./feh-empty-settings.nix;
+  feh-bindings = ./feh-bindings.nix;
+}
diff --git a/tests/modules/programs/feh/feh-bindings-expected-buttons b/tests/modules/programs/feh/feh-bindings-expected-buttons
new file mode 100644
index 000000000..f285ada64
--- /dev/null
+++ b/tests/modules/programs/feh/feh-bindings-expected-buttons
@@ -0,0 +1,4 @@
+zoom_in
+next_img C-4
+prev_img 3 C-3
+zoom_out 4
diff --git a/tests/modules/programs/feh/feh-bindings-expected-keys b/tests/modules/programs/feh/feh-bindings-expected-keys
new file mode 100644
index 000000000..ad558e6aa
--- /dev/null
+++ b/tests/modules/programs/feh/feh-bindings-expected-keys
@@ -0,0 +1,3 @@
+zoom_in
+prev_img h Left
+zoom_out minus
diff --git a/tests/modules/programs/feh/feh-bindings.nix b/tests/modules/programs/feh/feh-bindings.nix
new file mode 100644
index 000000000..f6b9e5b6e
--- /dev/null
+++ b/tests/modules/programs/feh/feh-bindings.nix
@@ -0,0 +1,33 @@
+{ pkgs, ... }:
+
+{
+  config = {
+    programs.feh.enable = true;
+
+    programs.feh.buttons = {
+      zoom_in = null;
+      zoom_out = 4;
+      next_img = "C-4";
+      prev_img = [ 3 "C-3" ];
+    };
+
+    programs.feh.keybindings = {
+      zoom_in = null;
+      zoom_out = "minus";
+      prev_img = [ "h" "Left" ];
+    };
+
+    nixpkgs.overlays =
+      [ (self: super: { feh = pkgs.writeScriptBin "dummy-feh" ""; }) ];
+
+    nmt.script = ''
+      assertFileContent \
+        home-files/.config/feh/buttons \
+        ${./feh-bindings-expected-buttons}
+
+      assertFileContent \
+        home-files/.config/feh/keys \
+        ${./feh-bindings-expected-keys}
+    '';
+  };
+}
diff --git a/tests/modules/programs/feh/feh-empty-settings.nix b/tests/modules/programs/feh/feh-empty-settings.nix
new file mode 100644
index 000000000..ad0d15153
--- /dev/null
+++ b/tests/modules/programs/feh/feh-empty-settings.nix
@@ -0,0 +1,15 @@
+{ pkgs, ... }:
+
+{
+  config = {
+    programs.feh.enable = true;
+
+    nixpkgs.overlays =
+      [ (self: super: { feh = pkgs.writeScriptBin "dummy-feh" ""; }) ];
+
+    nmt.script = ''
+      assertPathNotExists home-files/.config/feh/buttons
+      assertPathNotExists home-files/.config/feh/keys
+    '';
+  };
+}