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}" + ''; }