feat(keyboard): per-device layout for external keyboards

Add nomarchy.keyboard.devices ({ "<hyprctl-device-name>" = { layout; variant; }; })
generating 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 built-in one.
Hyprland applies it whenever that device connects, so the external keyboard
"remembers" its layout the declarative way. A Waybar hyprland/language
indicator shows the active layout, placed only when more than one layout is
in play (a comma in the session layout, or any per-device override) so
single-layout bars stay clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-15 17:27:28 +01:00
parent 0897391cfb
commit aef45b3c8d
5 changed files with 68 additions and 7 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = "<tt><small>{calendar}</small></tt>";
};
# 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;
}