183 lines
6.7 KiB
Nix
183 lines
6.7 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 {
|
|
# ===========================================
|
|
# INITRD: Apply remap BEFORE LUKS decryption
|
|
# ===========================================
|
|
|
|
# Enable systemd in initrd (required for this approach)
|
|
boot.initrd.systemd.enable = true;
|
|
|
|
# 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
|
|
'';
|
|
};
|
|
}
|