Nix-Vibe public snapshot (squashed history)
Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally collapsed to a single commit; this repo mirrors only the latest state.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# HP battery charge limiter (HP ENVY x360 / similar, board 81AD).
|
||||
#
|
||||
# HP firmware exposes no percentage-based charge threshold (no
|
||||
# charge_control_end_threshold sysfs on this machine). Instead the DSDT
|
||||
# exposes ACPI charge-mode methods in the root scope (\):
|
||||
#
|
||||
# \SBCC <n> -> miscellaneous battery charge control register writes
|
||||
# \SBCO <n> -> set battery charge operation:
|
||||
# 0x0500 = inhibit charge (stop charging / hold level)
|
||||
# 0x0000 = auto (normal charging) <-- restores on 81AD
|
||||
# 0x0200 = force discharge (drain while on AC)
|
||||
# \GBCC -> read back charge control
|
||||
# \GBCO -> read back charge operation mode
|
||||
#
|
||||
# These are driven from userspace through the `acpi_call` kernel module
|
||||
# (/proc/acpi/call). The "limit to 80%" is implemented as a policy on top:
|
||||
# a root systemd poller inhibits charge once capacity >= 80% and re-enables
|
||||
# auto charging below ~75%. This mirrors what the HP kernel-driver RFC
|
||||
# (platform/x86: hp-wmi charge behaviour support) describes leaving to
|
||||
# userspace while only the mode toggles live in the firmware.
|
||||
#
|
||||
# Control from the shell / CLI:
|
||||
# battery-charge-limit on|off|status
|
||||
# `on`/`off` persist the choice to /var/lib/hp-battery-charge-limit/state
|
||||
# and are applied immediately; the systemd service re-applies the policy
|
||||
# on a poll tick. petere has NOPASSWD sudo so the QuickShell cog can call
|
||||
# `sudo battery-charge-limit on/off` directly.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.my.hardware.hpBatteryLimit;
|
||||
|
||||
stateDir = "/var/lib/hp-battery-charge-limit";
|
||||
stateFile = "${stateDir}/state";
|
||||
|
||||
# charge mode constants read from Arg0 by SBCO (high byte).
|
||||
acpiInhibit = ''\\SBCO 0x0500'';
|
||||
acpiAuto = ''\\SBCO 0x0000'';
|
||||
|
||||
controlScript = pkgs.writeShellScriptBin "battery-charge-limit" ''
|
||||
set -eu
|
||||
|
||||
state_dir=${stateDir}
|
||||
state_file=${stateFile}
|
||||
battery=/sys/class/power_supply/BAT0
|
||||
acpi_proc=/proc/acpi/call
|
||||
|
||||
stop_at=${toString cfg.stopAt}
|
||||
resume_at=${toString cfg.resumeAt}
|
||||
|
||||
acpi() {
|
||||
# $1 = ACPI method + args, e.g. "\SBCO 0x0500"
|
||||
if [ ! -e "$acpi_proc" ]; then
|
||||
echo "acpi_call module not loaded" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$1" > "$acpi_proc" || return 1
|
||||
cat "$acpi_proc" || true
|
||||
}
|
||||
|
||||
read_capacity() {
|
||||
cat "$battery/capacity" 2>/dev/null | tr -d '\n' || echo 0
|
||||
}
|
||||
|
||||
ensure_state() {
|
||||
mkdir -p "$state_dir"
|
||||
if [ ! -f "$state_file" ]; then
|
||||
echo "${if cfg.defaultOn then "on" else "off"}" > "$state_file"
|
||||
fi
|
||||
}
|
||||
|
||||
get_state() {
|
||||
ensure_state
|
||||
cat "$state_file"
|
||||
}
|
||||
|
||||
apply_policy() {
|
||||
local state capacity
|
||||
state=$(get_state)
|
||||
capacity=$(read_capacity)
|
||||
case "$state" in
|
||||
on)
|
||||
if [ "$capacity" -ge "$stop_at" ]; then
|
||||
acpi "${acpiInhibit}" || true
|
||||
elif [ "$capacity" -le "$resume_at" ]; then
|
||||
acpi "${acpiAuto}" || true
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# off (or unknown) -> normal charging
|
||||
acpi "${acpiAuto}" || true
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
on)
|
||||
ensure_state
|
||||
echo on > "$state_file"
|
||||
apply_policy
|
||||
echo on
|
||||
;;
|
||||
off)
|
||||
ensure_state
|
||||
echo off > "$state_file"
|
||||
apply_policy
|
||||
echo off
|
||||
;;
|
||||
status)
|
||||
get_state
|
||||
;;
|
||||
apply)
|
||||
apply_policy
|
||||
;;
|
||||
*)
|
||||
echo "usage: battery-charge-limit {on|off|status|apply}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.my.hardware.hpBatteryLimit = {
|
||||
enable = lib.mkEnableOption "HP battery charge limiter (stop charging at stopAt%, re-enable below resumeAt).";
|
||||
stopAt = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 80;
|
||||
description = "Charge capacity % at which charging is inhibited.";
|
||||
};
|
||||
resumeAt = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 75;
|
||||
description = "Charge capacity % below which auto charging is re-enabled.";
|
||||
};
|
||||
defaultOn = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Start with limiting active on first boot (state file not present yet).";
|
||||
};
|
||||
pollInterval = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 120;
|
||||
description = "Seconds between policy re-applications by the systemd poller.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# acpi_call -> /proc/acpi/call so userspace can invoke SBCC/SBCO.
|
||||
boot.extraModulePackages = [ config.boot.kernelPackages.acpi_call ];
|
||||
boot.kernelModules = [ "acpi_call" ];
|
||||
|
||||
environment.systemPackages = [
|
||||
controlScript
|
||||
# Compatibility alias: older QuickShell configs referenced the
|
||||
# hp-prefixed binary name; keep it working until all hosts redeploy.
|
||||
(pkgs.writeShellScriptBin "hp-battery-charge-limit" ''
|
||||
exec ${controlScript}/bin/battery-charge-limit "$@"
|
||||
'')
|
||||
];
|
||||
|
||||
systemd.services.battery-charge-limit = {
|
||||
description = "HP battery charge limiter (${toString cfg.stopAt}% cap)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "systemd-modules-load.service" ];
|
||||
wants = [ "systemd-modules-load.service" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = pkgs.writeShellScript "battery-charge-limit-loop" ''
|
||||
set -eu
|
||||
trap 'exit 0' TERM INT
|
||||
while true; do
|
||||
${controlScript}/bin/battery-charge-limit apply >/dev/null || true
|
||||
sleep ${toString cfg.pollInterval}
|
||||
done
|
||||
'';
|
||||
Restart = "on-failure";
|
||||
RemainAfterExit = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.my.hardware.laptop;
|
||||
in
|
||||
{
|
||||
options.my.hardware.laptop = {
|
||||
enable = lib.mkEnableOption "Laptop-specific hardware support (fingerprint reader).";
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Fingerprint authentication - laptops only.
|
||||
# (fwupd is enabled globally via modules/core/management.nix)
|
||||
services.fprintd.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.my.hardware.nvidia;
|
||||
in
|
||||
{
|
||||
options.my.hardware.nvidia = {
|
||||
enable = lib.mkEnableOption "NVIDIA proprietary driver configuration";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.package;
|
||||
default = null;
|
||||
description = "NVIDIA driver package, or null to use the kernel's default.";
|
||||
};
|
||||
|
||||
nvidiaSettings = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable the nvidia-settings GUI tool.";
|
||||
};
|
||||
|
||||
powerManagement = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Whether to enable NVIDIA power management (persistence mode).";
|
||||
};
|
||||
|
||||
enable32Bit = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Whether to enable 32-bit OpenGL/Vulkan libraries.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
hardware.graphics = {
|
||||
enable = true;
|
||||
enable32Bit = cfg.enable32Bit;
|
||||
};
|
||||
|
||||
services.xserver.videoDrivers = [ "nvidia" ];
|
||||
|
||||
hardware.nvidia = {
|
||||
modesetting.enable = true;
|
||||
open = false; # Proprietary driver (pre-Turing/legacy architectures)
|
||||
nvidiaSettings = cfg.nvidiaSettings;
|
||||
powerManagement.enable = cfg.powerManagement;
|
||||
package = lib.mkIf (cfg.package != null) cfg.package;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Tablet mode support for 2-in-1 convertibles (currently wired to the
|
||||
# ThinkPad X1 Yoga Gen 6).
|
||||
#
|
||||
# In tablet mode (screen folded flat behind the keyboard) the display should
|
||||
# follow the device orientation and an on-screen keyboard should appear when a
|
||||
# text field is focused. This module provides that in three pieces:
|
||||
#
|
||||
# 1. The NixOS half (here): grants petere membership of the `input` group so
|
||||
# the user-level daemon can read the tablet-mode evdev switch
|
||||
# (/dev/input/event17, "Intel HID switches", SW_TABLET_MODE), and wires
|
||||
# in the home-manager module (hyprland-tablet.nix) via sharedModules.
|
||||
#
|
||||
# 2. A user-level Python daemon (`hyprland-tablet` daemon) that:
|
||||
# - watches SW_TABLET_MODE via python-evdev;
|
||||
# - parses `monitor-sensor` output for accelerometer orientation
|
||||
# (iio-sensor-proxy, already running on this host);
|
||||
# - applies `transform` to the eDP-1 monitor (and matching per-device
|
||||
# transforms for touch/tablet inputs) via hyprctl;
|
||||
# - starts/stops `wvkbd --auto` so a virtual keyboard shows when a text
|
||||
# input is focused while in tablet mode.
|
||||
#
|
||||
# 3. A `hyprland-tablet` CLI (status / keyboard toggle) used by the
|
||||
# QuickShell bar chip and the SUPER+CTRL+K keybind.
|
||||
#
|
||||
# Why not iio-hyprland? It rotates on every accelerometer reading regardless
|
||||
# of tablet mode; this host's iio-sensor-proxy build has no hinge support, so
|
||||
# orientation must be gated on the SW_TABLET_MODE switch ourselves.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.my.hardware.tabletMode;
|
||||
in
|
||||
{
|
||||
options.my.hardware.tabletMode = {
|
||||
enable = lib.mkEnableOption "tablet-mode auto-rotation + on-screen keyboard for 2-in-1 convertibles";
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# petere needs to read /dev/input/event17 (root:input 660) for the
|
||||
# SW_TABLET_MODE switch. The Intel HID switch device is unique to this
|
||||
# Yoga, so we match it explicitly and grant it to petere. This works for
|
||||
# the current session immediately -- no re-login needed to pick up a group.
|
||||
users.users.petere.extraGroups = lib.mkBefore [ "input" ];
|
||||
services.udev.extraRules = ''
|
||||
KERNEL=="event*", SUBSYSTEM=="input", ATTRS{name}=="Intel HID switches", OWNER="petere", MODE="0660", TAG+="uaccess"
|
||||
'';
|
||||
|
||||
home-manager.sharedModules = [
|
||||
../../home-manager/modules/hyprland-tablet.nix
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# ThinkPad battery charge limiter (native sysfs thresholds).
|
||||
#
|
||||
# Unlike the HP module (modules/hardware/hp-battery-limit.nix), ThinkPad
|
||||
# firmware exposes the battery charge thresholds directly via sysfs:
|
||||
#
|
||||
# /sys/class/power_supply/BAT0/charge_control_start_threshold (resume point)
|
||||
# /sys/class/power_supply/BAT0/charge_control_end_threshold (stop point)
|
||||
#
|
||||
# Writing a value < 100 to the end threshold stops charging at that capacity;
|
||||
# charging resumes once capacity drops to/below the start threshold. The EC
|
||||
# persists these across reboots (firmware NVRAM), so unlike the HP /proc magic
|
||||
# no periodic poller is required — a one-shot `apply` at boot re-asserts the
|
||||
# persisted choice in case the firmware was reset.
|
||||
#
|
||||
# Control from the shell / CLI (same interface as the HP module, so the
|
||||
# QuickShell cog can drive either host):
|
||||
# battery-charge-limit on|off|status|apply
|
||||
# `on`/`off` persist the choice to /var/lib/battery-charge-limit/state and are
|
||||
# applied immediately. petere has NOPASSWD sudo so the QuickShell cog can call
|
||||
# `sudo battery-charge-limit on/off` directly.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.my.hardware.thinkpadBatteryLimit;
|
||||
|
||||
stateDir = "/var/lib/battery-charge-limit";
|
||||
stateFile = "${stateDir}/state";
|
||||
|
||||
sysfs = "/sys/class/power_supply/BAT0";
|
||||
|
||||
controlScript = pkgs.writeShellScriptBin "battery-charge-limit" ''
|
||||
set -eu
|
||||
|
||||
state_dir=${stateDir}
|
||||
state_file=${stateFile}
|
||||
start_threshold=${sysfs}/charge_control_start_threshold
|
||||
end_threshold=${sysfs}/charge_control_end_threshold
|
||||
capacity=${sysfs}/capacity
|
||||
|
||||
stop_at=${toString cfg.stopAt}
|
||||
resume_at=${toString cfg.resumeAt}
|
||||
|
||||
read_end_threshold() {
|
||||
cat "$end_threshold" 2>/dev/null || echo 100
|
||||
}
|
||||
|
||||
ensure_state() {
|
||||
mkdir -p "$state_dir"
|
||||
if [ ! -f "$state_file" ]; then
|
||||
# Infer the current firmware state so the toggle reflects reality on
|
||||
# first run (e.g. a limit set from BIOS/Vantage persists in the EC).
|
||||
if [ "$(read_end_threshold)" -lt "$stop_at" ]; then
|
||||
echo on > "$state_file"
|
||||
else
|
||||
echo off > "$state_file"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
get_state() {
|
||||
ensure_state
|
||||
cat "$state_file"
|
||||
}
|
||||
|
||||
apply_policy() {
|
||||
local state
|
||||
state=$(get_state)
|
||||
case "$state" in
|
||||
on)
|
||||
# Enable the limit: resume charging below resume_at, stop at stop_at.
|
||||
echo "$resume_at" > "$start_threshold"
|
||||
echo "$stop_at" > "$end_threshold"
|
||||
;;
|
||||
*)
|
||||
# off (or unknown) -> full charge range (0..100).
|
||||
echo 0 > "$start_threshold"
|
||||
echo 100 > "$end_threshold"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
on)
|
||||
ensure_state
|
||||
echo on > "$state_file"
|
||||
apply_policy
|
||||
echo on
|
||||
;;
|
||||
off)
|
||||
ensure_state
|
||||
echo off > "$state_file"
|
||||
apply_policy
|
||||
echo off
|
||||
;;
|
||||
status)
|
||||
get_state
|
||||
;;
|
||||
apply)
|
||||
apply_policy
|
||||
;;
|
||||
*)
|
||||
echo "usage: battery-charge-limit {on|off|status|apply}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.my.hardware.thinkpadBatteryLimit = {
|
||||
enable = lib.mkEnableOption "ThinkPad battery charge limiter (stop charging at stopAt%, re-enable below resumeAt%).";
|
||||
stopAt = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 80;
|
||||
description = "Charge capacity % at which charging is inhibited (charge_control_end_threshold).";
|
||||
};
|
||||
resumeAt = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 75;
|
||||
description = "Charge capacity % below which auto charging is re-enabled (charge_control_start_threshold).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = [ controlScript ];
|
||||
|
||||
# One-shot re-assert at boot (ThinkPad EC persists thresholds, so no
|
||||
# poller is needed; this only recovers from firmware resets).
|
||||
systemd.services.battery-charge-limit = {
|
||||
description = "ThinkPad battery charge limiter (${toString cfg.stopAt}% cap)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "${controlScript}/bin/battery-charge-limit apply";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user