From 7fd54a50d8b2eaba709ecf1071154a3a1f00a0bc Mon Sep 17 00:00:00 2001 From: OliMoli Date: Fri, 31 Jul 2026 23:27:14 +0200 Subject: [PATCH] feat(home): add keybind generator script --- modules/home/lib/generators/generators.nix | 19 +++ modules/home/lib/generators/keybindings.py | 129 +++++++++++++++++++++ modules/home/scroll.nix | 114 ++---------------- 3 files changed, 158 insertions(+), 104 deletions(-) create mode 100644 modules/home/lib/generators/generators.nix create mode 100755 modules/home/lib/generators/keybindings.py diff --git a/modules/home/lib/generators/generators.nix b/modules/home/lib/generators/generators.nix new file mode 100644 index 0000000..e6cdb94 --- /dev/null +++ b/modules/home/lib/generators/generators.nix @@ -0,0 +1,19 @@ +{ + cfg, + pkgs, + moduleName, +}: +{ + keybindings = + if (cfg.package == null) then + { } + else + import (pkgs.runCommand "sway-keybindings" + { + nativeBuildInputs = [ pkgs.python3 ]; + } + '' + python ${./keybindings.py} ${cfg.package}/etc/${moduleName}/config > $out + '' + ) { inherit cfg; }; +} diff --git a/modules/home/lib/generators/keybindings.py b/modules/home/lib/generators/keybindings.py new file mode 100755 index 0000000..82c892a --- /dev/null +++ b/modules/home/lib/generators/keybindings.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +""" +Converts the Sway config into a Nix attribute set. + +Why? +If keybindings were provided manually, they would have to also be updated manually. +This would add more maintenance overhead. + +Why Python? +It's a lot simpler and more readable to use Python over Nix for something like this. +""" + +from collections import defaultdict +import sys + +CONFIG = sys.argv[1] + + +def escape(s: str) -> str: + """Basic function to escape some of the Nix expressions""" + return s.replace("\\", "\\\\").replace('"', '\\"').replace("${", "''${") + + +try: + with open(CONFIG) as f: + lines: list[str] = f.readlines() +except: + print("{}") + sys.exit() + +keybindings: dict[str, str] = {} +modes: defaultdict[str, dict[str, str]] = defaultdict(dict) +current_mode: str | None = None + +for line in lines: + stripped: str = line.strip() + + if stripped == "}" and current_mode is not None: + # Exit mode section: + # } + current_mode = None + + elif stripped.startswith("mode"): + # Enter mode section: + # mode "mode" { + + # Remove mode prefix + nomode: str = "mode".join(stripped.split("mode")[1:]).strip() + + if nomode.startswith('"'): + # Get name from in between the quotes + # (not implementing escaping since it isn't necessary) + current_mode = nomode.split('"')[1] + elif nomode.startswith("'"): + # Same as above but with single quotes + current_mode = nomode.split("'")[1] + else: + # Get name until "{" + # (not implementing one line mode definitions since those don't appear in def config) + current_mode = nomode.split("{")[0].strip() + + elif stripped.startswith("bindsym"): + # Parse bindsym line: + # bindsym [--args] keycombo command + + # Remove bindsym + nobindsym: str = "bindsym".join(stripped.split("bindsym")[1:]).strip() + + # Then iterate through each word + key_list: list[str] = [] + value_list: list[str] = [] + + bind_added: bool = False + + for word in nobindsym.split(" "): + if word.startswith("--") and not bind_added: + # Add args + key_list.append(word) + elif not bind_added: + # Add keycombo + key_list.append(word) + bind_added = True + else: + # Add command (rest of the words) + value_list.append(word) + + key: str = ( + escape(" ".join(key_list)) + .replace("$mod", "${cfg.config.modifier}") + .replace("$term", "${cfg.config.terminal}") + .replace("$menu", "${cfg.config.menu}") + .replace("$left", "${cfg.config.left}") + .replace("$right", "${cfg.config.right}") + .replace("$up", "${cfg.config.up}") + .replace("$down", "${cfg.config.down}") + ) + value: str = escape(" ".join(value_list)) + + if current_mode is None: + keybindings.update({key: value}) + else: + # Here defaultdict will create the current_mode key automatically + # so we don't have to check if it exists + modes[current_mode].update({key: value}) + +print("{ cfg }:") +print("{") + +print(" keybindings = {") + +for keycombo, command in keybindings.items(): + print(f' "{keycombo}" = "{command}";') + +print(" };") + +print(" modes = {") + +for mode, binds in modes.items(): + print(f' "{mode}" = ' + "{") + + for keycombo, command in binds.items(): + print(f' "{keycombo}" = "{command}";') + + print(" };") + +print("};") + +print("}") diff --git a/modules/home/scroll.nix b/modules/home/scroll.nix index 1dbefad..c43781f 100644 --- a/modules/home/scroll.nix +++ b/modules/home/scroll.nix @@ -32,6 +32,14 @@ let capitalModuleName = "Scroll"; }; + generators = import ./lib/generators/generators.nix { + inherit + cfg + pkgs + ; + moduleName = "scroll"; + }; + configModule = types.submodule { options = { inherit (commonOptions) @@ -85,75 +93,7 @@ let keybindings = mkOption { type = types.attrsOf (types.nullOr types.str); # https://github.com/dawsers/scroll/blob/master/config.in#L129 - default = lib.mapAttrs (_n: lib.mkOptionDefault) { - # TODO: add scroll keybindings - "${cfg.config.modifier}+Return" = "exec ${cfg.config.terminal}"; - "${cfg.config.modifier}+Shift+q" = "kill"; - "${cfg.config.modifier}+d" = "exec ${cfg.config.menu}"; - - "${cfg.config.modifier}+${cfg.config.left}" = "focus left"; - "${cfg.config.modifier}+${cfg.config.down}" = "focus down"; - "${cfg.config.modifier}+${cfg.config.up}" = "focus up"; - "${cfg.config.modifier}+${cfg.config.right}" = "focus right"; - - "${cfg.config.modifier}+Left" = "focus left"; - "${cfg.config.modifier}+Down" = "focus down"; - "${cfg.config.modifier}+Up" = "focus up"; - "${cfg.config.modifier}+Right" = "focus right"; - - "${cfg.config.modifier}+Shift+${cfg.config.left}" = "move left"; - "${cfg.config.modifier}+Shift+${cfg.config.down}" = "move down"; - "${cfg.config.modifier}+Shift+${cfg.config.up}" = "move up"; - "${cfg.config.modifier}+Shift+${cfg.config.right}" = "move right"; - - "${cfg.config.modifier}+Shift+Left" = "move left"; - "${cfg.config.modifier}+Shift+Down" = "move down"; - "${cfg.config.modifier}+Shift+Up" = "move up"; - "${cfg.config.modifier}+Shift+Right" = "move right"; - - "${cfg.config.modifier}+b" = "splith"; - "${cfg.config.modifier}+v" = "splitv"; - "${cfg.config.modifier}+f" = "fullscreen toggle"; - "${cfg.config.modifier}+a" = "focus parent"; - - "${cfg.config.modifier}+s" = "layout stacking"; - "${cfg.config.modifier}+w" = "layout tabbed"; - "${cfg.config.modifier}+e" = "layout toggle split"; - - "${cfg.config.modifier}+Shift+space" = "floating toggle"; - "${cfg.config.modifier}+space" = "focus mode_toggle"; - - "${cfg.config.modifier}+1" = "workspace number 1"; - "${cfg.config.modifier}+2" = "workspace number 2"; - "${cfg.config.modifier}+3" = "workspace number 3"; - "${cfg.config.modifier}+4" = "workspace number 4"; - "${cfg.config.modifier}+5" = "workspace number 5"; - "${cfg.config.modifier}+6" = "workspace number 6"; - "${cfg.config.modifier}+7" = "workspace number 7"; - "${cfg.config.modifier}+8" = "workspace number 8"; - "${cfg.config.modifier}+9" = "workspace number 9"; - "${cfg.config.modifier}+0" = "workspace number 10"; - - "${cfg.config.modifier}+Shift+1" = "move container to workspace number 1"; - "${cfg.config.modifier}+Shift+2" = "move container to workspace number 2"; - "${cfg.config.modifier}+Shift+3" = "move container to workspace number 3"; - "${cfg.config.modifier}+Shift+4" = "move container to workspace number 4"; - "${cfg.config.modifier}+Shift+5" = "move container to workspace number 5"; - "${cfg.config.modifier}+Shift+6" = "move container to workspace number 6"; - "${cfg.config.modifier}+Shift+7" = "move container to workspace number 7"; - "${cfg.config.modifier}+Shift+8" = "move container to workspace number 8"; - "${cfg.config.modifier}+Shift+9" = "move container to workspace number 9"; - "${cfg.config.modifier}+Shift+0" = "move container to workspace number 10"; - - "${cfg.config.modifier}+Shift+minus" = "move scratchpad"; - "${cfg.config.modifier}+minus" = "scratchpad show"; - - "${cfg.config.modifier}+Shift+c" = "reload"; - "${cfg.config.modifier}+Shift+e" = - "exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'"; - - "${cfg.config.modifier}+r" = "mode resize"; - }; + default = lib.mapAttrs (_n: lib.mkOptionDefault) generators.keybindings.keybindings; defaultText = "Default scroll keybindings."; description = '' An attribute set that assigns a key press to an action using a key symbol. @@ -259,41 +199,7 @@ let modes = mkOption { type = types.attrsOf (types.attrsOf types.str); - default = { - resize = { - "${cfg.config.left}" = "resize shrink width 10 px"; - "${cfg.config.down}" = "resize grow height 10 px"; - "${cfg.config.up}" = "resize shrink height 10 px"; - "${cfg.config.right}" = "resize grow width 10 px"; - "Left" = "resize shrink width 10 px"; - "Down" = "resize grow height 10 px"; - "Up" = "resize shrink height 10 px"; - "Right" = "resize grow width 10 px"; - "Escape" = "mode default"; - "Return" = "mode default"; - }; - # TODO: add more modes - }; - defaultText = lib.literalExpression '' - { - resize = { - # Binds arrow keys to resizing commands - ''${cfg.config.left}" = "resize shrink width 10 px"; - ''${cfg.config.down}" = "resize grow height 10 px"; - ''${cfg.config.up}" = "resize shrink height 10 px"; - ''${cfg.config.right}" = "resize grow width 10 px"; - - "Left" = "resize shrink width 10 px"; - "Down" = "resize grow height 10 px"; - "Up" = "resize shrink height 10 px"; - "Right" = "resize grow width 10 px"; - - # Exit resize mode - "Escape" = "mode default"; - "Return" = "mode default"; - }; - } - ''; + default = generators.keybindings.modes; description = '' An attribute set that defines binding modes and keybindings inside them