nixos/frame12/modules/colemak-ec.nix

186 lines
6.8 KiB
Nix

# Colemak-DH EC Keyboard Remap for Framework Laptop
#
# TROUBLESHOOTING NOTES (for future agents):
# ==========================================
# This module applies keyboard remaps via the Framework EC (Embedded Controller).
# EC remaps are VOLATILE - lost after ~10 seconds unplugged (cold boot).
#
# If remap doesn't work at LUKS prompt after cold boot:
#
# 1. Check if service ran:
# journalctl -b -u colemak-ec-remap
#
# 2. Enable verbose boot for debugging:
# boot.initrd.verbose = lib.mkForce true;
# boot.kernelParams = lib.mkForce [ "boot.shell_on_fail" ];
#
# 3. Verify EC modules loaded in initrd:
# lsinitrd /boot/initrd | grep cros_ec
#
# 4. Check service ordering - must run BEFORE cryptsetup.target
#
# 5. If systemd initrd approach fails entirely, fallback options:
# - FrameworkHacksPkg EFI driver: https://github.com/DHowett/FrameworkHacksPkg
# - This runs before OS starts, survives cold boots
#
# References:
# - https://www.howett.net/posts/2021-12-framework-ec/
# - https://deck.sh/using-the-ec-to-change-the-keyboard-layout-on-the-framework-laptop/
# - https://wiki.archlinux.org/title/Framework_Laptop_13
{ config, pkgs, lib, ... }:
let
# All ectool remap commands
remapCommands = ectoolPath: ''
# Colemak-DH layout via EC for Framework 12
# Matrix positions verified via FRAME12_KEY_MATRIX.md scan
# Top row (QWERTY Colemak-DH)
${ectoolPath} raw 0x3E0C d1,d1,b7,b8,w2b # e(7,8) f
${ectoolPath} raw 0x3E0C d1,d1,b7,b9,w4d # r(7,9) p
${ectoolPath} raw 0x3E0C d1,d1,b2,b3,w32 # t(2,3) b
${ectoolPath} raw 0x3E0C d1,d1,b2,b6,w3b # y(2,6) j
${ectoolPath} raw 0x3E0C d1,d1,b7,b1,w4b # u(7,1) l
${ectoolPath} raw 0x3E0C d1,d1,b7,b2,w3c # i(7,2) u
${ectoolPath} raw 0x3E0C d1,d1,b7,b3,w35 # o(7,3) y
${ectoolPath} raw 0x3E0C d1,d1,b7,b4,w4c # p(7,4) ;
# Home row (QWERTY Colemak-DH)
${ectoolPath} raw 0x3E0C d1,d1,b3,b4,w2d # s(3,4) r
${ectoolPath} raw 0x3E0C d1,d1,b4,b2,w1b # d(4,2) s
${ectoolPath} raw 0x3E0C d1,d1,b4,b3,w2c # f(4,3) t
${ectoolPath} raw 0x3E0C d1,d1,b1,b6,w3a # h(1,6) m
${ectoolPath} raw 0x3E0C d1,d1,b4,b6,w31 # j(4,6) n
${ectoolPath} raw 0x3E0C d1,d1,b4,b5,w24 # k(4,5) e
${ectoolPath} raw 0x3E0C d1,d1,b4,b9,w43 # l(4,9) i
${ectoolPath} raw 0x3E0C d1,d1,b4,b8,w44 # ;(4,8) o
# Bottom row with angle mod (QWERTY Colemak-DH)
${ectoolPath} raw 0x3E0C d1,d1,b6,b1,w22 # z(6,1) x
${ectoolPath} raw 0x3E0C d1,d1,b5,b8,w21 # x(5,8) c
${ectoolPath} raw 0x3E0C d1,d1,b5,b5,w23 # c(5,5) d
${ectoolPath} raw 0x3E0C d1,d1,b0,b3,w1a # b(0,3) z
${ectoolPath} raw 0x3E0C d1,d1,b0,b5,w42 # n(0,5) k
${ectoolPath} raw 0x3E0C d1,d1,b5,bb,w33 # m(5,b) h
'';
# Smart remap: check first, only apply if needed (for systemd service)
# Reads position (7,8) - if scancode is 0x2b (f), remap is already applied
smartRemapScript = ectoolPath: ''
# Read current scancode at e-key position (7,8)
# Response has scancode at bytes 10-11; we check byte 10 for 0x2b (f)
CURRENT=$(${ectoolPath} raw 0x3E0C d1,d0,b7,b8,w0 2>/dev/null | grep '|' | head -1 | awk '{print $11}')
if [ "$CURRENT" = "2b" ]; then
echo "Colemak-DH remap already active, skipping."
exit 0
fi
echo "Applying Colemak-DH EC remap..."
${remapCommands ectoolPath}
echo "Colemak-DH remap applied."
'';
# Script with retry logic for initrd
initrdRemapScript = pkgs.writeShellScript "colemak-ec-initrd-remap" ''
set -e
ECTOOL="${pkgs.fw-ectool}/bin/ectool"
# Wait for EC driver to be ready (up to 3 seconds)
echo "Waiting for EC driver..."
for i in $(seq 1 30); do
if $ECTOOL version >/dev/null 2>&1; then
echo "EC driver ready after $((i * 100))ms"
break
fi
sleep 0.1
done
# Verify EC communication works
if ! $ECTOOL version >/dev/null 2>&1; then
echo "ERROR: Cannot communicate with EC after 3 seconds"
exit 1
fi
echo "Applying Colemak-DH EC remap..."
${remapCommands "$ECTOOL"}
echo "Colemak-DH remap applied successfully"
'';
in
{
options.colemakEc.enable = lib.mkEnableOption "Colemak-DH EC keyboard remap";
config = lib.mkIf config.colemakEc.enable {
# Require systemd initrd (set in default.nix for Plymouth compatibility)
assertions = [{
assertion = config.boot.initrd.systemd.enable;
message = "colemakEc requires boot.initrd.systemd.enable = true";
}];
# ===========================================
# INITRD: Apply remap BEFORE LUKS decryption
# ===========================================
# Add kernel modules for EC communication
boot.initrd.availableKernelModules = [ "cros_ec" "cros_ec_lpcs" ];
# Add fw-ectool and the remap script to initrd
boot.initrd.systemd.storePaths = [ pkgs.fw-ectool initrdRemapScript ];
# Systemd service that runs before cryptsetup
# Key: wantedBy sysinit.target + requiredBy on cryptsetup units ensures
# this service runs before LUKS password prompt
boot.initrd.systemd.services.colemak-ec-remap = {
description = "Apply Colemak-DH keyboard remap to EC";
wantedBy = [ "sysinit.target" ];
before = [ "cryptsetup.target" "systemd-ask-password-console.service" ];
after = [ "systemd-modules-load.service" ];
wants = [ "systemd-modules-load.service" ];
unitConfig = {
DefaultDependencies = false;
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = initrdRemapScript;
StandardOutput = "journal+console";
StandardError = "journal+console";
};
};
# Make cryptsetup units wait for our remap service
# This adds After= and Wants= dependencies to each LUKS device's cryptsetup unit
boot.initrd.systemd.services."systemd-cryptsetup@" = {
after = [ "colemak-ec-remap.service" ];
wants = [ "colemak-ec-remap.service" ];
};
# ===========================================
# SYSTEMD: Apply remap after boot (backup)
# ===========================================
# This ensures the remap is applied even if the EC state was reset,
# and provides a service that can be restarted manually if needed.
systemd.services.colemak-ec-remap = {
description = "Apply Colemak-DH remap to EC keyboard";
after = [ "multi-user.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.writeShellScript "colemak-dh-remap" ''
${smartRemapScript "${pkgs.fw-ectool}/bin/ectool"}
''}";
};
};
# Also run after resume from suspend/hibernate
powerManagement.resumeCommands = ''
systemctl start colemak-ec-remap
'';
};
}