diff --git a/configs/nixos/modules/desktop/session/hyprland/default.nix b/configs/nixos/modules/desktop/session/hyprland/default.nix index 86c3874..14bb2da 100644 --- a/configs/nixos/modules/desktop/session/hyprland/default.nix +++ b/configs/nixos/modules/desktop/session/hyprland/default.nix @@ -4,10 +4,228 @@ ... }: # The Hyprland compositor. +let + directionalKeybinds = lib.listToAttrs ( + lib.concatLists ( + lib.mapAttrsToList + (key: direction: [ + { + name = "hyprland-focus-${direction}"; + value = { + keys = [ + "SUPER" + key + ]; + action = { + type = "focus"; + inherit direction; + }; + }; + } + { + name = "hyprland-move-window-${direction}"; + value = { + keys = [ + "SUPER" + "SHIFT" + key + ]; + action = { + type = "move-window"; + inherit direction; + }; + }; + } + ]) + { + H = "left"; + J = "down"; + K = "up"; + L = "right"; + } + ) + ); + + resizeKeybinds = lib.listToAttrs ( + lib.mapAttrsToList + (key: delta: { + name = "hyprland-resize-${delta.name}"; + value = { + keys = [ + "SUPER" + "ALT" + key + ]; + action = { + type = "resize-window"; + inherit (delta) x y; + }; + }; + }) + { + H = { + name = "left"; + x = -40; + y = 0; + }; + J = { + name = "down"; + x = 0; + y = 40; + }; + K = { + name = "up"; + x = 0; + y = -40; + }; + L = { + name = "right"; + x = 40; + y = 0; + }; + } + ); + + workspaceKeybinds = lib.listToAttrs ( + lib.concatMap ( + workspace: + let + workspaceName = toString workspace; + in + [ + { + name = "hyprland-focus-workspace-${workspaceName}"; + value = { + keys = [ + "SUPER" + workspaceName + ]; + action = { + type = "focus-workspace"; + inherit workspace; + }; + }; + } + { + name = "hyprland-move-to-workspace-${workspaceName}"; + value = { + keys = [ + "SUPER" + "SHIFT" + workspaceName + ]; + action = { + type = "move-to-workspace"; + inherit workspace; + }; + }; + } + ] + ) (lib.range 1 9) + ); + + mkBind = keybind: { + _args = [ + (lib.concatStringsSep " + " keybind.keys) + (mkDispatcher keybind.action) + ]; + }; + + mkDispatcher = + action: + lib.generators.mkLuaInline ( + { + exec = "hl.dsp.exec_cmd(${toLua action.command})"; + focus = "hl.dsp.focus(${toLua { inherit (action) direction; }})"; + "move-window" = "hl.dsp.window.move(${toLua { inherit (action) direction; }})"; + "resize-window" = "hl.dsp.window.resize(${ + toLua ({ + inherit (action) x y; + relative = true; + }) + })"; + "toggle-floating" = "hl.dsp.window.float(${toLua { action = "toggle"; }})"; + fullscreen = "hl.dsp.window.fullscreen()"; + layout = "hl.dsp.layout(${toLua action.message})"; + "close-window" = "hl.dsp.window.close()"; + "kill-window" = "hl.dsp.window.kill()"; + "focus-workspace" = "hl.dsp.focus(${toLua { inherit (action) workspace; }})"; + "move-to-workspace" = "hl.dsp.window.move(${toLua { inherit (action) workspace; }})"; + } + .${action.type} + ); + + keybinds = lib.attrValues ( + lib.attrByPath [ config.networking.hostName ] { } config.registry.desktop-keybind + ); + + toLua = lib.generators.toLua { }; +in { options.modules.desktop.session.hyprland.enable = lib.mkEnableOption "the Hyprland compositor"; config = lib.mkIf config.modules.desktop.session.hyprland.enable { + registry.desktop-keybind.${config.networking.hostName} = { + hyprland-terminal = { + keys = [ + "SUPER" + "Return" + ]; + action = { + type = "exec"; + command = "ghostty"; + }; + }; + + hyprland-toggle-floating = { + keys = [ + "SUPER" + "Space" + ]; + action.type = "toggle-floating"; + }; + + hyprland-fullscreen = { + keys = [ + "SUPER" + "F" + ]; + action.type = "fullscreen"; + }; + + hyprland-toggle-split = { + keys = [ + "SUPER" + "T" + ]; + action = { + type = "layout"; + message = "togglesplit"; + }; + }; + + hyprland-close-window = { + keys = [ + "SUPER" + "SHIFT" + "Q" + ]; + action.type = "close-window"; + }; + + hyprland-kill-window = { + keys = [ + "SUPER" + "CTRL" + "Q" + ]; + action.type = "kill-window"; + }; + } + // directionalKeybinds + // resizeKeybinds + // workspaceKeybinds; + # This program integration owns the session, portals, and polkit, launched # through the universal Wayland session manager. programs.hyprland = { @@ -34,14 +252,7 @@ settings = { config.general.layout = "dwindle"; - bind = [ - { - _args = [ - "SUPER + Return" - (lib.generators.mkLuaInline ''hl.dsp.exec_cmd("ghostty")'') - ]; - } - ]; + bind = map mkBind keybinds; }; }; }; diff --git a/configs/nixos/registry/desktop-keybind.nix b/configs/nixos/registry/desktop-keybind.nix new file mode 100644 index 0000000..78eb351 --- /dev/null +++ b/configs/nixos/registry/desktop-keybind.nix @@ -0,0 +1,133 @@ +{ lib, ... }: +let + actionMatchesType = + action: + let + required = actionFields.${action.type}; + in + lib.all (field: action.${field} != null) required + && lib.all (field: lib.elem field required || action.${field} == null) actionOptions; + + actionFields = { + exec = [ "command" ]; + focus = [ "direction" ]; + "move-window" = [ "direction" ]; + "resize-window" = [ + "x" + "y" + ]; + "toggle-floating" = [ ]; + fullscreen = [ ]; + layout = [ "message" ]; + "close-window" = [ ]; + "kill-window" = [ ]; + "focus-workspace" = [ "workspace" ]; + "move-to-workspace" = [ "workspace" ]; + }; + + actionOptions = [ + "command" + "direction" + "x" + "y" + "workspace" + "message" + ]; +in +{ + checks = [ + { + assertion = entries: lib.all (entry: entry.value.keys != [ ]) entries; + message = "Desktop keybinds must set at least one key."; + } + { + assertion = entries: lib.all (entry: actionMatchesType entry.value.action) entries; + message = "Desktop keybind action fields must match their type."; + } + ]; + + type = lib.types.submodule { + options = { + keys = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = [ + "SUPER" + "Return" + ]; + description = "Sets the ordered key parts for this desktop keybind."; + }; + + action = lib.mkOption { + type = lib.types.submodule { + options = { + type = lib.mkOption { + type = lib.types.enum [ + "exec" + "focus" + "move-window" + "resize-window" + "toggle-floating" + "fullscreen" + "layout" + "close-window" + "kill-window" + "focus-workspace" + "move-to-workspace" + ]; + description = "Selects the desktop action this keybind triggers."; + }; + + command = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "ghostty"; + description = "Command executed by an exec action."; + }; + + direction = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "left" + "down" + "up" + "right" + ] + ); + default = null; + description = "Direction used by focus and move-window actions."; + }; + + x = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + example = 40; + description = "Horizontal delta used by resize-window actions."; + }; + + y = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + example = 40; + description = "Vertical delta used by resize-window actions."; + }; + + workspace = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + example = 1; + description = "Workspace number used by workspace actions."; + }; + + message = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "togglesplit"; + description = "Layout message sent by layout actions."; + }; + }; + }; + description = "Declares the desktop action for this keybind."; + }; + }; + }; +}