From 6f7074d21dfa50e2e30b3e087cca60abd1c0f9f8 Mon Sep 17 00:00:00 2001
From: Thiago Kenji Okada <thiagokokada@gmail.com>
Date: Sat, 23 Jan 2021 12:30:34 -0300
Subject: [PATCH] rofi: migrate to rasi configuration format (#1736)

* rofi: migrate to rasi configuration format

The Xresources configuration format is deprecated in Rofi. For example,
using Rofi from unstable (1.6.1 as of now) you get the following
warnings when starting the application:

```
(process:9272): Rofi-WARNING **: 01:38:48.596: The old Xresources based configuration format is deprecated.

(process:9272): Rofi-WARNING **: 01:38:48.596: Please upgrade: rofi -upgrade-config.
``````

So this commit migrates it for its new configuration format, called rasi
instead.

This new implementation uses attrsets manipulation instead of using
strings, making the code clearer and also fixing some bugs found during
the way. To make sure everything is right, I also created some tests.

If someone wants to validate if the generated config is correct, just
run in terminal:

```
$ rofi -dump-config
```

And rofi will dump the current configuration file, including all
unsetted options.

* docs: document programs.rofi.extraConfig changes

* rofi: add thiagokokada as maintainer

* rofi: add toRasi function
---
 .github/CODEOWNERS                            |   6 +-
 doc/release-notes/rl-2103.adoc                |  22 ++++
 modules/programs/rofi.nix                     | 122 +++++++++++-------
 tests/modules/programs/rofi/default.nix       |   1 +
 .../programs/rofi/valid-config-expected.rasi  |  20 +++
 tests/modules/programs/rofi/valid-config.nix  |  57 ++++++++
 6 files changed, 178 insertions(+), 50 deletions(-)
 create mode 100644 tests/modules/programs/rofi/valid-config-expected.rasi
 create mode 100644 tests/modules/programs/rofi/valid-config.nix

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9bd93e1b6..f8ae353c5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -91,7 +91,8 @@
 
 /modules/programs/mcfly.nix                           @marsam
 
-/modules/programs/mpv.nix                             @tadeokondrak
+/modules/programs/mpv.nix                             @tadeokondrak @thiagokokada
+/tests/modules/programs/mpv                           @thiagokokada
 
 /modules/programs/mu.nix                              @KarlJoad
 
@@ -123,6 +124,9 @@
 
 /modules/programs/powerline-go.nix                    @DamienCassou
 
+/modules/programs/rofi.nix                            @thiagokokada
+/tests/modules/programs/rofi                          @thiagokokada
+
 /modules/programs/rofi-pass.nix                       @seylerius
 /tests/modules/programs/rofi-pass                     @seylerius
 
diff --git a/doc/release-notes/rl-2103.adoc b/doc/release-notes/rl-2103.adoc
index 599af75f2..db33e0cfe 100644
--- a/doc/release-notes/rl-2103.adoc
+++ b/doc/release-notes/rl-2103.adoc
@@ -50,6 +50,28 @@ As a result of this change, <<opt-programs.mpv.package>> is no longer the
 resulting derivation. Use the newly introduced `programs.mpv.finalPackage`
 instead.
 
+* The <<opt-programs.rofi.extraConfig>> option is now an attrset rather
+than a string. To migrate, move the each line into the attrset,
+removing the `rofi.` prefix from the keys. For example,
++
+[source,nix]
+----
+programs.rofi.extraConfig = ''
+  rofi.show-icons: true
+  rofi.modi: drun,emoji,ssh
+'';
+----
++
+becomes
++
+[source,nix]
+----
+programs.rofi.extraConfig = {
+  show-icons = true;
+  modi = "drun,emoji,ssh";
+};
+----
+
 [[sec-release-21.03-state-version-changes]]
 === State Version Changes
 
diff --git a/modules/programs/rofi.nix b/modules/programs/rofi.nix
index 734bcc423..f7b4682be 100644
--- a/modules/programs/rofi.nix
+++ b/modules/programs/rofi.nix
@@ -71,21 +71,9 @@ let
     };
   };
 
-  valueToString = value:
-    if isBool value then (if value then "true" else "else") else toString value;
-
   windowColorsToString = window:
     concatStringsSep ", " (with window; [ background border separator ]);
 
-  rowsColorsToString = rows: ''
-    ${optionalString (rows.normal != null)
-    (setOption "color-normal" (rowColorsToString rows.normal))}
-    ${optionalString (rows.active != null)
-    (setOption "color-active" (rowColorsToString rows.active))}
-    ${optionalString (rows.urgent != null)
-    (setOption "color-urgent" (rowColorsToString rows.urgent))}
-  '';
-
   rowColorsToString = row:
     concatStringsSep ", " (with row; [
       background
@@ -95,14 +83,45 @@ let
       highlight.foreground
     ]);
 
-  setOption = name: value:
-    optionalString (value != null) "rofi.${name}: ${valueToString value}";
+  mkColorScheme = colors:
+    if colors != null then
+      with colors; {
+        color-window =
+          if (window != null) then (windowColorsToString window) else null;
+        color-normal = if (rows != null && rows.normal != null) then
+          (rowColorsToString rows.normal)
+        else
+          null;
+        color-active = if (rows != null && rows.active != null) then
+          (rowColorsToString rows.active)
+        else
+          null;
+        color-urgent = if (rows != null && rows.active != null) then
+          (rowColorsToString rows.urgent)
+        else
+          null;
+      }
+    else
+      { };
 
-  setColorScheme = colors:
-    optionalString (colors != null) ''
-      ${optionalString (colors.window != null) setOption "color-window"
-      (windowColorsToString colors.window)}
-      ${optionalString (colors.rows != null) (rowsColorsToString colors.rows)}
+  mkValueString = value:
+    if isBool value then
+      if value then "true" else "false"
+    else if isInt value then
+      toString value
+    else
+      ''"${toString value}"'';
+
+  mkKeyValue = name: value: "${name}: ${mkValueString value};";
+
+  toRasi = section: config:
+    let
+      # Remove null values so the resulting config does not have empty lines
+      configStr = generators.toKeyValue { inherit mkKeyValue; }
+        (attrsets.filterAttrs (m: v: v != null) config);
+    in ''
+      ${section} {
+      ${configStr}}
     '';
 
   locationsMap = {
@@ -119,12 +138,12 @@ let
 
   themeName = if (cfg.theme == null) then
     null
-  else if (lib.isString cfg.theme) then
+  else if (isString cfg.theme) then
     cfg.theme
   else
-    lib.removeSuffix ".rasi" (baseNameOf cfg.theme);
+    removeSuffix ".rasi" (baseNameOf cfg.theme);
 
-  themePath = if (lib.isString cfg.theme) then null else cfg.theme;
+  themePath = if (isString cfg.theme) then null else cfg.theme;
 
 in {
   options.programs.rofi = {
@@ -220,7 +239,7 @@ in {
 
     location = mkOption {
       default = "center";
-      type = types.enum (builtins.attrNames locationsMap);
+      type = types.enum (attrNames locationsMap);
       description = "The location rofi appears on the screen.";
     };
 
@@ -284,15 +303,22 @@ in {
     };
 
     configPath = mkOption {
-      default = "${config.xdg.configHome}/rofi/config";
-      defaultText = "$XDG_CONFIG_HOME/rofi/config";
+      default = "${config.xdg.configHome}/rofi/config.rasi";
+      defaultText = "$XDG_CONFIG_HOME/rofi/config.rasi";
       type = types.str;
       description = "Path where to put generated configuration file.";
     };
 
     extraConfig = mkOption {
-      default = "";
-      type = types.lines;
+      default = { };
+      example = literalExample ''
+        {
+          modi = "drun,emoji,ssh";
+          kb-primary-paste = "Control+V,Shift+Insert";
+          kb-secondary-paste = "Control+v,Insert";
+        }
+      '';
+      type = with types; attrsOf (oneOf [ int str bool ]);
       description = "Additional configuration to add.";
     };
 
@@ -308,31 +334,29 @@ in {
 
     home.packages = [ cfg.package ];
 
-    home.file."${cfg.configPath}".text = ''
-      ${setOption "width" cfg.width}
-      ${setOption "lines" cfg.lines}
-      ${setOption "font" cfg.font}
-      ${setOption "bw" cfg.borderWidth}
-      ${setOption "eh" cfg.rowHeight}
-      ${setOption "padding" cfg.padding}
-      ${setOption "separator-style" cfg.separator}
-      ${setOption "hide-scrollbar"
-      (if (cfg.scrollbar != null) then (!cfg.scrollbar) else cfg.scrollbar)}
-      ${setOption "terminal" cfg.terminal}
-      ${setOption "cycle" cfg.cycle}
-      ${setOption "fullscreen" cfg.fullscreen}
-      ${setOption "location" (builtins.getAttr cfg.location locationsMap)}
-      ${setOption "xoffset" cfg.xoffset}
-      ${setOption "yoffset" cfg.yoffset}
-
-      ${setColorScheme cfg.colors}
-      ${setOption "theme" themeName}
-
-      ${cfg.extraConfig}
-    '';
+    home.file."${cfg.configPath}".text = toRasi "configuration" ({
+      width = cfg.width;
+      lines = cfg.lines;
+      font = cfg.font;
+      bw = cfg.borderWidth;
+      eh = cfg.rowHeight;
+      padding = cfg.padding;
+      separator-style = cfg.separator;
+      hide-scrollbar =
+        if (cfg.scrollbar != null) then (!cfg.scrollbar) else null;
+      terminal = cfg.terminal;
+      cycle = cfg.cycle;
+      fullscreen = cfg.fullscreen;
+      location = (getAttr cfg.location locationsMap);
+      xoffset = cfg.xoffset;
+      yoffset = cfg.yoffset;
+      theme = themeName;
+    } // (mkColorScheme cfg.colors) // cfg.extraConfig);
 
     xdg.dataFile = mkIf (themePath != null) {
       "rofi/themes/${themeName}.rasi".source = themePath;
     };
   };
+
+  meta.maintainers = with maintainers; [ thiagokokada ];
 }
diff --git a/tests/modules/programs/rofi/default.nix b/tests/modules/programs/rofi/default.nix
index c18c3b0ed..03b9049e6 100644
--- a/tests/modules/programs/rofi/default.nix
+++ b/tests/modules/programs/rofi/default.nix
@@ -1,3 +1,4 @@
 {
+  rofi-valid-config = ./valid-config.nix;
   rofi-assert-on-both-theme-and-colors = ./assert-on-both-theme-and-colors.nix;
 }
diff --git a/tests/modules/programs/rofi/valid-config-expected.rasi b/tests/modules/programs/rofi/valid-config-expected.rasi
new file mode 100644
index 000000000..23cf24606
--- /dev/null
+++ b/tests/modules/programs/rofi/valid-config-expected.rasi
@@ -0,0 +1,20 @@
+configuration {
+bw: 1;
+color-normal: "argb:58455a64, #fafbfc, argb:58455a64, #00bcd4, #fafbfc";
+color-window: "argb:583a4c54, argb:582a373e, #c3c6c8";
+cycle: false;
+eh: 1;
+font: "Droid Sans Mono 14";
+hide-scrollbar: false;
+kb-primary-paste: "Control+V,Shift+Insert";
+kb-secondary-paste: "Control+v,Insert";
+lines: 10;
+location: 0;
+modi: "drun,emoji,ssh";
+padding: 400;
+separator-style: "solid";
+terminal: "/some/path";
+width: 100;
+xoffset: 0;
+yoffset: 0;
+}
diff --git a/tests/modules/programs/rofi/valid-config.nix b/tests/modules/programs/rofi/valid-config.nix
new file mode 100644
index 000000000..8fb66a7b8
--- /dev/null
+++ b/tests/modules/programs/rofi/valid-config.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  config = {
+    programs.rofi = {
+      enable = true;
+      width = 100;
+      lines = 10;
+      borderWidth = 1;
+      rowHeight = 1;
+      padding = 400;
+      font = "Droid Sans Mono 14";
+      scrollbar = true;
+      terminal = "/some/path";
+      separator = "solid";
+      cycle = false;
+      fullscren = true;
+      colors = {
+        window = {
+          background = "argb:583a4c54";
+          border = "argb:582a373e";
+          separator = "#c3c6c8";
+        };
+
+        rows = {
+          normal = {
+            background = "argb:58455a64";
+            foreground = "#fafbfc";
+            backgroundAlt = "argb:58455a64";
+            highlight = {
+              background = "#00bcd4";
+              foreground = "#fafbfc";
+            };
+          };
+        };
+      };
+      window = {
+        background = "background";
+        border = "border";
+        separator = "separator";
+      };
+      extraConfig = {
+        modi = "drun,emoji,ssh";
+        kb-primary-paste = "Control+V,Shift+Insert";
+        kb-secondary-paste = "Control+v,Insert";
+      };
+    };
+
+    nmt.script = ''
+      assertFileContent \
+        home-files/.config/rofi/config.rasi \
+        ${./valid-config-expected.rasi}
+    '';
+  };
+}