diff --git a/lib/rofi.nix b/lib/rofi.nix
index 416a6847..c5371894 100644
--- a/lib/rofi.nix
+++ b/lib/rofi.nix
@@ -3,79 +3,164 @@
pkgs,
lib,
...
-}: rec {
+}: {
# Receives attrs like:
# {
# "Poweroff" = "poweroff";
# "Reload Hyprland" = "hyprctl reload";
# }
- mkSimpleMenu = let
- # Makes a string like ''"Poweroff" "Reload Hyprland"''
- unpack-options = attrs: "\"${lib.concatStringsSep "\" \"" (builtins.attrNames attrs)}\"";
+ # mkSimpleMenu = let
+ # # Makes a string like ''"Poweroff" "Reload Hyprland"''
+ # unpack-options = attrs: "\"${lib.concatStringsSep "\" \"" (builtins.attrNames attrs)}\"";
+ #
+ # mkCase = option: action: "else if test \"${option}\" = $OPTION\n set ACTION \"${action}\"";
+ #
+ # cases = attrs:
+ # attrs
+ # |> builtins.mapAttrs mkCase
+ # |> builtins.attrValues
+ # |> builtins.concatStringsSep "\n";
+ # in
+ # {
+ # prompt,
+ # attrs,
+ # command ? ''rofi -dmenu -i'',
+ # }:
+ # pkgs.writeScriptBin "rofi-menu-${prompt}" ''
+ # #! ${pkgs.fish}/bin/fish
+ #
+ # # OPTIONS contains all possible values Rofi will display
+ # set OPTIONS ${unpack-options attrs}
+ #
+ # # We choose a single OPTION using Rofi
+ # set OPTION (echo -e (string join "\n" $OPTIONS) | ${command} -p "${prompt}")
+ #
+ # # Check if the chosen OPTION is a valid choice from OPTIONS
+ # if not contains $OPTION $OPTIONS
+ # exit
+ # end
+ #
+ # # Set a command to execute based on the chosen OPTION
+ # if false
+ # exit # Easier to generate with this
+ # ${cases attrs}
+ # else
+ # exit
+ # end
+ #
+ # # Execute the command
+ # eval $ACTION
+ # '';
- mkCase = option: action: "else if test \"${option}\" = $OPTION\n set ACTION \"${action}\"";
+ # Rofi/Dmenu menu generator.
+ #
+ # Each element in `layers` is one of:
+ # - attrset { "Label" = "value"; } # static options: selected value → $OPTIONn
+ # - string "shell-cmd" # dynamic options from command: selected text → $OPTIONn
+ # # may reference $OPTION0, $OPTION1, ... from earlier layers
+ #
+ # The "command" is the last action, it can reference $OPTION0, $OPTION1, ...
+ # If no "command" is given and the last layer is a static attrset, its selected value is evaluated directly.
+ #
+ # The "prompts" list are optional per-layer prompt strings (falls back to "prompt" if not provided).
+ #
+ # vpn.fish equivalent:
+ # mkMenu {
+ # prompt = "vpn";
+ # layers = [
+ # "cat /etc/rofi-vpns"
+ # { "start" = "start"; "stop" = "stop"; "status" = "status"; }
+ # ];
+ # command = "systemctl $OPTION1 $OPTION0.service";
+ # }
+ #
+ # lectures.fish equivalent:
+ # mkMenu {
+ # prompt = "lecture";
+ # layers = [
+ # "eza -1 -D ~/Notes/TU"
+ # "eza -1 ~/Notes/TU/$OPTION0/Lecture | grep '.pdf'"
+ # ];
+ # command = "xdg-open ~/Notes/TU/$OPTION0/Lecture/$OPTION1";
+ # }
+ mkMenu = {
+ prompt,
+ layers,
+ prompts ? [],
+ command ? null,
+ rofiCmd ? "rofi -dmenu -i",
+ }: let
+ isStaticLayer = layer: builtins.isAttrs layer && !(layer ? options);
+ isDynamicLayer = layer: builtins.isString layer;
- cases = attrs:
- attrs
- |> builtins.mapAttrs mkCase
- |> builtins.attrValues
- |> builtins.concatStringsSep "\n";
- in
- {
- prompt,
- attrs,
- command ? ''rofi -dmenu -p " ${prompt} " -i'',
- }:
- pkgs.writeScriptBin "rofi-menu-${prompt}" ''
- #! ${pkgs.fish}/bin/fish
+ escStr = s: builtins.replaceStrings [''"'' "\\"] [''\"'' "\\\\"] s;
- # OPTIONS contains all possible values Rofi will display
- set OPTIONS ${unpack-options attrs}
+ layerPrompt = i:
+ if i < builtins.length prompts
+ then lib.elemAt prompts i
+ else prompt;
- # We choose a single OPTION using Rofi
- set OPTION (echo -e (string join "\n" $OPTIONS) | ${command})
-
- # Check if the chosen OPTION is a valid choice from OPTIONS
- if not contains $OPTION $OPTIONS
+ # Static layer: attrset of label -> value
+ # Displays labels in rofi; maps selected label to its value -> $OPTIONi
+ mkStaticLayer = i: attrs: let
+ lp = layerPrompt i;
+ labels = builtins.attrNames attrs;
+ optsStr = "\"${lib.concatStringsSep "\" \"" (map escStr labels)}\"";
+ mkCase = label: value: "else if test \"${escStr label}\" = $_LABEL${toString i}\n set OPTION${toString i} \"${escStr value}\"";
+ casesStr =
+ builtins.concatStringsSep "\n"
+ (builtins.attrValues (builtins.mapAttrs mkCase attrs));
+ in {
+ script = ''
+ set _OPTS${toString i} ${optsStr}
+ set _LABEL${toString i} (echo -e (string join "\n" $_OPTS${toString i}) | ${rofiCmd} -p "${lp}")
+ if not contains $_LABEL${toString i} $_OPTS${toString i}
exit
end
-
- # Set a command to execute based on the chosen OPTION
if false
- exit # Easier to generate with this
- ${cases attrs}
+ exit
+ ${casesStr}
else
exit
end
-
- # Execute the command
- eval $ACTION
'';
+ };
- # TODO: I want to generate the containers menu using the actionsA and actionsB attrs:
- # - actionsA will be generated from the stuff in oci-containers.containers
- # - actionsB will be set statically for start, stop, status
+ # Dynamic layer: shell command string whose output is piped to rofi
+ # Selected text -> $OPTIONi; may reference earlier $OPTIONn variables
+ mkDynamicLayer = i: cmd: let
+ lp = layerPrompt i;
+ in {
+ script = ''
+ set OPTION${toString i} (${cmd} | ${rofiCmd} -p "${lp}")
+ if test -z $OPTION${toString i}
+ exit
+ end
+ '';
+ };
- # Receives attrs like:
- # {
- # optionA = "exa -1 -D ~/Notes/TU";
- # optionB = "exa -1 -D ~/Notes/TU/$OPTIONA/Lecture | grep \".pdf\"";
- # commandB = "xdg-open ~/Notes/TU/$OPTIONA/Lecture/$OPTIONB";
- # }
- #
- # Keys:
- # - optionA, optionB # Command that generates Rofi options:
- # exa -1 -D ~/Notes/TU
- # cat /etc/rofi-vpns
- # - commandA, commandB # Action to execute after sth. was chosen (mutually excl. with command)
- # - actionsA, actionsB # Configure actions by lookup (mutually excl. with command):
- # actionsB = {"status" = "systemctl status..."}
- # - colorA, colorB # Configure highlighting conditions:
- # colorA = {"red" = "systemctl ... | grep ..."};
- #
- # Use $OPTIONA and $OPTIONB to use the options chosen by option-command and rofi
- # Use $EVALA and $EVALB to use the outputs generated by command
- mkMenu = let
+ mkLayer = i: layer:
+ if isStaticLayer layer
+ then mkStaticLayer i layer
+ else if isDynamicLayer layer
+ then mkDynamicLayer i layer
+ else throw "mkMenu: layer ${toString i} has invalid type";
+
+ layerResults = lib.imap0 mkLayer layers; # Map with 0-based index
+ layerScripts = map (r: r.script) layerResults;
+ lastLayer = lib.last layers;
+
+ finalCmd =
+ if command != null
+ then command
+ else if isStaticLayer lastLayer
+ then "$OPTION${toString (builtins.length layers - 1)}"
+ else throw "mkMenu: 'command' must be set when the last layer is not a static attrset";
in
- prompt: attrs: "";
+ pkgs.writeScriptBin "rofi-menu-${prompt}" ''
+ #! ${pkgs.fish}/bin/fish
+
+ ${lib.concatStringsSep "\n" layerScripts}
+ eval "${finalCmd}"
+ '';
}