1

Lib/Rofi: Replace mkSimpleMenu with multi-layer-menu capable mkMenu

This commit is contained in:
2026-03-27 01:18:02 +01:00
parent 84fec63204
commit e1d041f010

View File

@ -3,79 +3,164 @@
pkgs, pkgs,
lib, lib,
... ...
}: rec { }: {
# Receives attrs like: # Receives attrs like:
# { # {
# "Poweroff" = "poweroff"; # "Poweroff" = "poweroff";
# "Reload Hyprland" = "hyprctl reload"; # "Reload Hyprland" = "hyprctl reload";
# } # }
mkSimpleMenu = let # mkSimpleMenu = let
# Makes a string like ''"Poweroff" "Reload Hyprland"'' # # Makes a string like ''"Poweroff" "Reload Hyprland"''
unpack-options = attrs: "\"${lib.concatStringsSep "\" \"" (builtins.attrNames attrs)}\""; # 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: escStr = s: builtins.replaceStrings [''"'' "\\"] [''\"'' "\\\\"] s;
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
# OPTIONS contains all possible values Rofi will display layerPrompt = i:
set OPTIONS ${unpack-options attrs} if i < builtins.length prompts
then lib.elemAt prompts i
else prompt;
# We choose a single OPTION using Rofi # Static layer: attrset of label -> value
set OPTION (echo -e (string join "\n" $OPTIONS) | ${command}) # Displays labels in rofi; maps selected label to its value -> $OPTIONi
mkStaticLayer = i: attrs: let
# Check if the chosen OPTION is a valid choice from OPTIONS lp = layerPrompt i;
if not contains $OPTION $OPTIONS 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 exit
end end
# Set a command to execute based on the chosen OPTION
if false if false
exit # Easier to generate with this exit
${cases attrs} ${casesStr}
else else
exit exit
end end
# Execute the command
eval $ACTION
''; '';
};
# TODO: I want to generate the containers menu using the actionsA and actionsB attrs: # Dynamic layer: shell command string whose output is piped to rofi
# - actionsA will be generated from the stuff in oci-containers.containers # Selected text -> $OPTIONi; may reference earlier $OPTIONn variables
# - actionsB will be set statically for start, stop, status 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: mkLayer = i: layer:
# { if isStaticLayer layer
# optionA = "exa -1 -D ~/Notes/TU"; then mkStaticLayer i layer
# optionB = "exa -1 -D ~/Notes/TU/$OPTIONA/Lecture | grep \".pdf\""; else if isDynamicLayer layer
# commandB = "xdg-open ~/Notes/TU/$OPTIONA/Lecture/$OPTIONB"; then mkDynamicLayer i layer
# } else throw "mkMenu: layer ${toString i} has invalid type";
#
# Keys: layerResults = lib.imap0 mkLayer layers; # Map with 0-based index
# - optionA, optionB # Command that generates Rofi options: layerScripts = map (r: r.script) layerResults;
# exa -1 -D ~/Notes/TU lastLayer = lib.last layers;
# cat /etc/rofi-vpns
# - commandA, commandB # Action to execute after sth. was chosen (mutually excl. with command) finalCmd =
# - actionsA, actionsB # Configure actions by lookup (mutually excl. with command): if command != null
# actionsB = {"status" = "systemctl status..."} then command
# - colorA, colorB # Configure highlighting conditions: else if isStaticLayer lastLayer
# colorA = {"red" = "systemctl ... | grep ..."}; then "$OPTION${toString (builtins.length layers - 1)}"
# else throw "mkMenu: 'command' must be set when the last layer is not a static attrset";
# Use $OPTIONA and $OPTIONB to use the options chosen by option<A/B>-command and rofi
# Use $EVALA and $EVALB to use the outputs generated by command<A/B>
mkMenu = let
in in
prompt: attrs: ""; pkgs.writeScriptBin "rofi-menu-${prompt}" ''
#! ${pkgs.fish}/bin/fish
${lib.concatStringsSep "\n" layerScripts}
eval "${finalCmd}"
'';
} }