Hyprland inefficiet lua config

Recently, hyprland has switched the configuration language from hyprlang to lua.
This involves a lot of calls to hyprland functions, for example binding a key is calling hl.bind() with the key combination and the function to be called on the keypress.

Configuring hyprland with homemanager, binds used to be a list of strings like so:

 bind = [
   "$mod, T, exec, kitty" # Launch terminal
 ];

Now, with the new lua config, every string in that bind will be the first parameter (the key) of the hl.bind() function, but that string needs to be escaped to actually call the proper function.
The only way I have found that can do this, is from an example on search.nixos.org (attached in full below), where the same bind would look like this:

      bind = [
        {
          _args = [
            (lib.generators.mkLuaInline "mod .. \" + T\"")
            (lib.generators.mkLuaInline "hl.dsp.exec_cmd(\"kitty\")")
          ];
        }
      ];

This seems like a very verbose and inefficient way to do it, adding 5 more lines to do the same thing.
I have thought of just having a lua file and linking it to the correct place with home manager, but I would like to use stylix for some color options, which would then be unable to write to the hyprland config.

Is there a better way to do this that I am missing?

If not, would it be possible to make a helper function to turn an attribute set of string=string into the proper bind list, so you’d have something that looked like this:

  bind = [
    generate_binds {
      "${mod} + T" ''hl.dsp.exec_cmd("kitty")''
    };
  ];

Full example from search.nixos.org:

{
  mod = {
    _var = "SUPER";
  };

  config = {
    general = {
      gaps_in = 5;
      gaps_out = 20;
      border_size = 2;
    };

    decoration = {
      rounding = 10;
    };
  };

  bind = [
    {
      _args = [
        (lib.generators.mkLuaInline "mod .. \" + Q\"")
        (lib.generators.mkLuaInline "hl.dsp.window.close()")
        { locked = true; }
      ];
    }
    {
      _args = [
        "SUPER + RETURN"
        (lib.generators.mkLuaInline "hl.dsp.exec_cmd(\"kitty\")")
      ];
    }
    {
      _args = [
        "ALT + R"
        (lib.generators.mkLuaInline "hl.dsp.submap(\"resize\")")
      ];
    }
  ];

  define_submap = {
    _args = [
      "resize"
      (lib.generators.mkLuaInline "function()\n  hl.bind(\"right\", hl.dsp.window.resize({ x = 10, y = 0, relative = true }), { repeating = true })\n  hl.bind(\"left\", hl.dsp.window.resize({ x = -10, y = 0, relative = true }), { repeating = true })\n  hl.bind(\"escape\", hl.dsp.submap(\"reset\"))\nend")
    ];
  };

  window_rule = {
    match.class = "kitty";
    border_size = 2;
  };

  on = {
    _args = [
      "hyprland.start"
      (lib.generators.mkLuaInline "function()\n  hl.exec_cmd(\"waybar\")\nend")
    ];
  };
}
4 Likes

Hi!

If what you actually want is to keep using hyprlang (.conf) for the time being, that’s still possible. Here’s a quick summary of how it works.

Config file lookup order in Hyprland 0.55+

  • Hyprland first looks for ~/.config/hypr/hyprland.lua.
  • If there’s no .lua, it falls back to the old hyprland.conf (hyprlang is still supported for backward compatibility).

So to go back to the old behavior

  • Keep configType = "hyprlang" on the Home Manager side so it generates hyprland.conf.
  • Then delete hyprland.lua.

After that, your .conf will be picked up again.

One gotcha: the auto-generated .lua

  • If you launch Hyprland while there’s neither a .lua nor a .conf in ~/.config/hypr/, Hyprland auto-generates an example hyprland.lua.
  • This auto-generated .lua is outside of Home Manager’s management, so Home Manager can’t remove it — you have to delete it manually with rm ~/.config/hypr/hyprland.lua. (If you make sure the .conf is in place before the first launch, it won’t be generated in the first place.)

That said, this is only a temporary workaround: hyprlang is scheduled to be removed in a future release, so keep in mind that you’ll eventually need to migrate to .lua.

And if you already knew all of this and your question was really about wanting to write your config in Lua (just without the verbosity), then sorry for the off-target answer :folded_hands:

(Also, I’m not a native English speaker, so apologies if my English sounds a bit off!)

I too am currently working on my Hyprland config. Here’s how I’m doing my keybinds:

bind =
  map
    (
      {
        keys,
        dispatcher,
        flags ? { },
      }:
      {
        _args = [
          keys
          (lib.generators.mkLuaInline dispatcher)
          flags
        ];
      }
    )
    [
      {
        keys = "SUPER + T";
        dispatcher = ''hl.dsp.exec_cmd("uwsm app -- kitty")'';
        flags.description = "Open kitty terminal emulator";
      }
      {
        keys = "XF86AudioRaiseVolume";
        dispatcher = ''hl.dsp.exec_cmd("dms ipc call audio increment 5")'';
        flags = {
          description = "Raise audio output volume";
          locked = true;
          repeating = true;
        };
      }
      # ...
    ];

Animations, curves, etc. follow the same logic. I find this approach cleaner than having to repeat _args and mkLuaInline on every bind, but it does now take 500 lines to do what used to take 100… Of course if there’s a better way, I’d like to know as well!

IMO Stylix should make a separate Lua file for hyprland config and import that in the main one, see:

Maybe make an issue for this?

I was having this exact same issue, the way I eventually figured out was like this:

settings = { };
extraConfig = builtins.readFile config/hyprland.lua;

Then I write my raw lua config in the imported file.

I was also having this issue and found the verbosity really painful, so I ended up writing a little flake that wraps a lot of the annoying stuff. If anyone else wants to use it I have it public on GitHub. It lets you define binds like

    bind = [
      { key = "SUPER + C"; handler = hl.dsp.window.close; }
      { key = "SUPER + M"; handler = hl.dsp.exit; }
      { key = "SUPER + T"; handler = hl.dsp.exec_cmd "kitty"; }

      # Use mkLuaInline directly for anything not covered by the helpers
      { key = "SUPER + V"; handler = lib.generators.mkLuaInline "hl.dsp.window.float({ action = 'toggle' })"; }
    ];

and on start commands like

    on.hyprland.start = [
      (hl.exec_cmd "waybar")
      (hl.exec_cmd "hyprpolkitagent")
    ];

I also found a way to inject a require without causing any errors, so you can add config for anything missing like

    extraConfig = ''
      hl.bind("SUPER + SHIFT + E", hl.dsp.exec_cmd("dolphin"))
    '';

and it gets included in addition to everything else.

Not my post, see Reddit - Please wait for verification for the post by positrone13103 but this looks very nice:

{ lib , ... }:
let
  lua = lib.generators.mkLuaInline;
  bind = key: action: {
    _args = [
      key
      (lua action)
    ];
  };
  exec = cmd: ''hl.dsp.exec_cmd("${cmd}")'';
  mvws = ws: ''hl.dsp.focus({ workspace = "${ws}"})'';
  mvwd = ws: ''hl.dsp.window.move({ workspace = "${ws}"})'';
  mvwddr = dr: ''hl.dsp.window.move({ direction = "${dr}"})'';
  fs = mode: ''hl.dsp.window.fullscreen({ mode = "${mode}"})'';
  focusdr = dr: ''hl.dsp.focus({ direction = "${dr}"})'';
in
{
  wayland.windowManager.hyprland.settings.bind = [
    #launch applications
    (bind "SUPER + SHIFT + F" (exec "firefox"))
    (bind "SUPER + CTRL + F" (exec "thunar"))
    (bind "SUPER + ALT + F" (exec "faugus-launcher"))
    (bind "SUPER + SHIFT + B" (exec "brave"))
    (bind "SUPER + CTRL + L" (exec "librewolf"))
    (bind "SUPER + SHIFT + H" (exec "heroic"))
    (bind "SUPER + SHIFT + J" (exec "jellyfin-desktop"))
    (bind "SUPER + SHIFT + L" (exec "lutris"))
    (bind "SUPER + SHIFT + N" (exec "hyprsunset"))
    (bind "SUPER + CTRL + N" (exec "pkill hyprsunset"))
    (bind "SUPER + SHIFT + P" (exec "firefox --private-window"))
    (bind "SUPER + CTRL + RETURN" (exec "kitty yazi"))
    (bind "CTRL + SHIFT + F" (exec "nautilus"))
    (bind "SUPER + CTRL + S" (exec "steam"))
    (bind "SUPER + SHIFT + T" (exec "tauon"))
    (bind "SUPER + SHIFT + V" (exec "vesktop"))
    (bind "SUPER + CTRL + V" (exec "flatpak run dev.vencord.Vesktop"))
    (bind "SUPER + SHIFT + ESCAPE" (exec "wlogout"))
    (bind "SUPER + H" (exec "cliphist list | wofi --dmenu | cliphist decode | wl-copy"))
    (bind "SUPER + CTRL + H" (exec "cliphist wipe"))
    (bind "SUPER + RETURN" (exec "kitty"))
    (bind "SUPER + D" (exec "wofi"))

    #actions 
    (bind "SUPER + SHIFT + W" (exec "pkill waybar; waybar"))
    (bind "SUPER + CTRL + W" (exec "pkill hyprpaper; hyprpaper"))
    (bind "SUPER + ALT + down" (exec "ddcutil setvcp 10 - 5 --bus 6 & ddcutil setvcp 10 - 5 --bus 5"))
    (bind "SUPER + ALT + UP" (exec "ddcutil setvcp 10 + 5 --bus 6 & ddcutil setvcp 10 + 5 --bus 5"))

    #audio
    (bind "XF86AudioRaiseVolume" (exec "wpctl set-volume @ 1%+"))
    (bind "XF86AudioLowerVolume" (exec "wpctl set-volume u/DEFAULT_SINK@ 1%-"))
    (bind "XF86AudioPlay" (exec "playerctl play-pause"))
    (bind "ALT + M" (exec ""))

    #screenshots
    (bind "SUPER + SHIFT + S" (exec "hyprshot -m region -z --clipboard only"))
    (bind "PRINT" (exec "hyprshot -m output -m active --clipboard only"))
    (bind "SUPER + PRINT" (exec "grim - | wl-copy"))

    #workspaces
    (bind "SUPER + 1" (mvws "1"))
    (bind "SUPER + 2" (mvws "2"))
    (bind "SUPER + 3" (mvws "3"))
    (bind "SUPER + 4" (mvws "4"))
    (bind "SUPER + 5" (mvws "5"))
    (bind "SUPER + 6" (mvws "6"))
    (bind "SUPER + 7" (mvws "7"))
    (bind "SUPER + 8" (mvws "8"))
    (bind "SUPER + 9" (mvws "9"))
    (bind "SUPER + 0" (mvws "10"))
    (bind "SUPER + F1" (mvws "11"))
    (bind "SUPER + F2" (mvws "12"))
    (bind "SUPER + F3" (mvws "13"))
    (bind "SUPER + F4" (mvws "14"))
    (bind "SUPER + F5" (mvws "15"))
    (bind "SUPER + F6" (mvws "16"))
    (bind "SUPER + F7" (mvws "17"))
    (bind "SUPER + F8" (mvws "18"))
    (bind "SUPER + F9" (mvws "19"))
    (bind "SUPER + F10" (mvws "20"))

    #windows and focus
    (bind "SUPER + F" (fs "maximized")) 
    (bind "SUPER + F11" (fs "fullscreen")) 
    (bind "SUPER + V" "hl.dsp.window.float({})")
    (bind "SUPER + J" "hl.dsp.layout(\"togglesplit\")")
    (bind "SUPER + mouse:272" "hl.dsp.window.drag()")
    (bind "SUPER + mouse:273" "hl.dsp.window.resize()")

    (bind "SUPER + SHIFT + up" (mvwddr "up"))
    (bind "SUPER + SHIFT + right" (mvwddr "right"))
    (bind "SUPER + SHIFT + down" (mvwddr "down"))
    (bind "SUPER + SHIFT + left" (mvwddr "left"))

    (bind "SUPER + up" (focusdr "up"))
    (bind "SUPER + right" (focusdr "right"))
    (bind "SUPER + down" (focusdr "down"))
    (bind "SUPER + left" (focusdr "left"))

    (bind "SUPER + ESCAPE" "hl.dsp.window.close()")

    (bind "SUPER + SHIFT + 1" (mvwd "1"))
    (bind "SUPER + SHIFT + 2" (mvwd "2"))
    (bind "SUPER + SHIFT + 3" (mvwd "3"))
    (bind "SUPER + SHIFT + 4" (mvwd "4"))
    (bind "SUPER + SHIFT + 5" (mvwd "5"))
    (bind "SUPER + SHIFT + 6" (mvwd "6"))
    (bind "SUPER + SHIFT + 7" (mvwd "7"))
    (bind "SUPER + SHIFT + 8" (mvwd "8"))
    (bind "SUPER + SHIFT + 9" (mvwd "9"))
    (bind "SUPER + SHIFT + 0" (mvwd "10"))
    (bind "SUPER + SHIFT + F1" (mvwd "11"))
    (bind "SUPER + SHIFT + F2" (mvwd "12"))
    (bind "SUPER + SHIFT + F3" (mvwd "13"))
    (bind "SUPER + SHIFT + F4" (mvwd "14"))
    (bind "SUPER + SHIFT + F5" (mvwd "15"))
    (bind "SUPER + SHIFT + F6" (mvwd "16"))
    (bind "SUPER + SHIFT + F7" (mvwd "17"))
    (bind "SUPER + SHIFT + F8" (mvwd "18"))
    (bind "SUPER + SHIFT + F9" (mvwd "19"))
    (bind "SUPER + SHIFT + F10" (mvwd "20"))
  ];
};

and

{ lib , ... }:
let
  lua = lib.generators.mkLuaInline;
  on = event: body: {
    _args = [
      event
      (lua ''function() ${body} end'')
    ];
  };
  exec = cmd: ''hl.exec_cmd("${cmd}")'';
in
{
  wayland.windowManager.hyprland.settings.on = [
    (
      on "hyprland.start" ''
        ${exec "waybar"}
        ${exec "hyprsunset"}
        ${exec "wl-paste --type text --watch cliphist store"}
        ${exec "wl-paste --type image --watch cliphist store"}
      ''
    )
  ];
};
1 Like