diff --git a/README.md b/README.md index 49e6e64..d8ef99a 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**. | `nomarchy.terminal` | `"ghostty"` | Terminal for keybinds and `$TERMINAL` | | `nomarchy.keyboard.layout` | `"us"` | XKB layout for the Hyprland session (installer writes the matching `services.xserver.xkb` in system.nix; the console + LUKS prompt follow via the distro default) | | `nomarchy.keyboard.variant` | `""` | XKB variant for the session | +| `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant | | `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config | | `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar | | `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f50b156..46b5fa5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -283,11 +283,21 @@ how to override it. Items marked ✓ are shipped. - **Night light / blue-light filter:** schedulable colour-temperature shift via `hyprsunset` (Hyprland-native; wlsunset/gammastep are alternatives) — sunset/sunrise or a fixed schedule, with a menu + Waybar toggle. -- **Runtime keyboard-layout switching:** the session layout is a single - `nomarchy.keyboard.layout` today. Support a list of layouts with a toggle - bind (`hyprctl switchxkblayout` / xkb `grp:` options) and a Waybar - indicator for the active layout. Natural follow-on to the LUKS-keymap work - — keep the system (console/initrd) and session layouts in sync. +- **Keyboard layouts (per-device + switching):** + - ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices` + (`{ "" = { layout; variant; }; }`) generates Hyprland + `device` blocks that override the session `nomarchy.keyboard.layout` for a + named keyboard — e.g. an external board that's physically a different + layout than the laptop's. Hyprland applies it whenever that device + connects (the "external keyboard remembers its layout" case, done the + declarative way). A Waybar `hyprland/language` indicator shows the active + layout, gated on more than one layout being in play (comma in the session + layout, or any per-device override) so single-layout bars stay clean. + - Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb + `grp:` options) for switching layouts on one keyboard; add the + `hyprland/language` module to the summer whole-swap themes for parity + (the gating doesn't translate to their static JSON). Keep the system + (console/initrd) and session layouts in sync (the LUKS-keymap work). - ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu (`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell indicator (`custom/notification` via `swaync-client -swb`: shows the diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index 668a4e2..f85653a 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -150,6 +150,14 @@ in touchpad.natural_scroll = lib.mkDefault true; }; + # Per-device keyboard layouts (nomarchy.keyboard.devices): each + # overrides the session kb_layout for one named keyboard — e.g. an + # external board that's physically a different layout than the laptop's + # built-in one. Hyprland applies them whenever the device connects. + device = lib.mapAttrsToList + (name: d: { inherit name; kb_layout = d.layout; kb_variant = d.variant; }) + config.nomarchy.keyboard.devices; + # dwindle:pseudotile was removed in Hyprland 0.55 (the `pseudo` # dispatcher still exists); setting it aborts config parsing. dwindle.preserve_split = lib.mkDefault true; diff --git a/modules/home/options.nix b/modules/home/options.nix index 7144989..f8f3bc9 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -96,6 +96,33 @@ in description = "XKB variant for the Hyprland session."; }; + keyboard.devices = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + layout = lib.mkOption { + type = lib.types.str; + description = "XKB layout for this specific keyboard."; + }; + variant = lib.mkOption { + type = lib.types.str; + default = ""; + description = "XKB variant for this keyboard."; + }; + }; + }); + default = { }; + example = lib.literalExpression ''{ "keychron-keychron-k2" = { layout = "de"; }; }''; + description = '' + Per-device keyboard layout. The key is the device name from + `hyprctl devices` (lower-case, hyphenated). Each entry generates a + Hyprland `device` block that overrides the session-wide + nomarchy.keyboard.layout for that keyboard only — e.g. an external + keyboard that's physically a different layout than the laptop's + built-in one. Hyprland applies it automatically whenever that + keyboard is connected. + ''; + }; + package = lib.mkOption { type = lib.types.package; default = pkgs.nomarchy-theme-sync; diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index 0fd0128..c07fdb3 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -12,6 +12,12 @@ let t = config.nomarchy.theme; + # Show the active-keyboard-layout indicator only when more than one layout + # is in play — multiple session layouts (comma-separated) or per-device + # overrides (nomarchy.keyboard.devices) — so single-layout bars stay clean. + showLanguage = lib.hasInfix "," config.nomarchy.keyboard.layout + || config.nomarchy.keyboard.devices != { }; + # Power-profile indicator (power-profiles-daemon). Both scripts self- # gate: they exit silently unless this is a laptop (a battery is # present) running PPD, so the module hides itself on desktops and @@ -70,7 +76,9 @@ let modules-left = [ "hyprland/workspaces" "hyprland/window" ]; modules-center = [ "clock" ]; - modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" "battery" "custom/notification" ]; + modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" ] + ++ lib.optional showLanguage "hyprland/language" + ++ [ "battery" "custom/notification" ]; "hyprland/workspaces" = { format = "{icon}"; @@ -88,6 +96,13 @@ let tooltip-format = "{calendar}"; }; + # Active keyboard layout (per focused device) — only placed in + # modules-right when showLanguage (see above). + "hyprland/language" = { + format = "󰌌 {short}"; + tooltip = false; + }; + pulseaudio = { format = "{icon} {volume}%"; format-muted = "󰝟"; @@ -193,7 +208,7 @@ let font-weight: bold; } - #tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #battery, #custom-notification { + #tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification { color: @subtext; padding: 0 10px; }