diff --git a/.gitmodules b/.gitmodules index 72cbd051e4..1801808079 100644 --- a/.gitmodules +++ b/.gitmodules @@ -646,3 +646,7 @@ [submodule "home/.local/share/nvim/site/pack/default/opt/snacks.nvim"] path = home/.local/share/nvim/site/pack/default/opt/snacks.nvim url = https://github.com/folke/snacks.nvim.git +[submodule "deps/acpilight"] + path = deps/acpilight + url = https://gitlab.com/wavexx/acpilight + ignore = untracked diff --git a/README.md b/README.md index c0c4dde854..07348608ff 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,50 @@ Custom uBlock Origin filters ("My filters") are declared in [`home/.mozilla/nati Notes: - A full browser restart is required to pick up changes to this file. - `toOverwrite` fully replaces the "My filters" pane content on every launch, so any filters added manually through the uBlock UI will be reverted on restart — treat this file as the source of truth and edit it instead. + +### Backlight control (acpilight) + +[`acpilight`](https://gitlab.com/wavexx/acpilight) provides a backward-compatible `xbacklight` replacement that controls brightness via `/sys/class/backlight` instead of X11's RandR `Backlight` property, so it works for laptop panels the RandR property was never wired up for, and it's driven by a redshift hook ([`home/.config/redshift/hooks/backlight`](home/.config/redshift/hooks/backlight)) that dims the display along with the day/night cycle. + +Controlling an external/desktop monitor's brightness this way additionally requires the **`ddcci-driver-linux`** kernel module, which exposes DDC/CI-controlled monitors under `/sys/class/backlight/ddcci*` (the same interface as a native panel), so the same hook script works unmodified on both a laptop (native panel) and a desktop (DDC/CI monitor): + +- **Arch Linux**: AUR package `ddcci-driver-linux-dkms` (or `-git` for newer kernels) +- **Slackware**: [`ddcci-driver-linux`](https://slackbuilds.org/repository/15.0/system/ddcci-driver-linux/) on SlackBuilds.org + +System settings required: + +- **Registering each monitor's DDC/CI device**, since `ddcci-driver-linux` doesn't autodetect monitors on its own -- it needs the main device (address `0x37`) added manually per i2c bus. Add the following snippet to your system's startup (adjust the `card0-*` glob to match which GPU your monitors are on): + ```sh + /sbin/modprobe ddcci-backlight + + # DisplayPort connectors expose the real DDC/CI-capable channel as a direct + # i2c-N subdirectory of the connector itself (the AUX-channel-backed + # adapter); their `ddc` symlink instead points to a separate, more limited + # legacy adapter that can't complete the full DDC/CI handshake. HDMI + # connectors have no such split -- their `ddc` symlink is the only (and + # correct) adapter. + ( + for conn in /sys/class/drm/card0-*/; do + [ "$(cat "$conn/status" 2>/dev/null)" = "connected" ] || continue + direct=$(ls -d "$conn"i2c-* 2>/dev/null | head -1) + if [ -n "$direct" ]; then + # DP + ddc_bus=$(basename "$direct") + else + # HDMI + ddc_link="$conn/ddc" + [ -e "$ddc_link" ] || continue + ddc_bus=$(basename "$(readlink -f "$ddc_link")") + fi + bus_num=${ddc_bus#i2c-} + [ -e "/sys/bus/ddcci/devices/ddcci$bus_num" ] && continue + echo ddcci 0x37 > "/sys/bus/i2c/devices/$ddc_bus/new_device" 2>/dev/null + done + ) & # writing to new_device is a synchronous sysfs call that blocks until the driver's real I2C handshake with that monitor finishes, and it's slow + ``` +- **Installing acpilight's udev rule** so `xbacklight` can write to `/sys/class/backlight` without root: + ```sh + sudo install -vCDt /etc/udev/rules.d deps/acpilight/90-backlight.rules + sudo udevadm trigger -s backlight -c add + ``` + (also requires your user to be in the `video` group) diff --git a/deps/acpilight b/deps/acpilight new file mode 160000 index 0000000000..3f9c62553f --- /dev/null +++ b/deps/acpilight @@ -0,0 +1 @@ +Subproject commit 3f9c62553f4c34c5e21439ae52f6a88155dbcc3c diff --git a/home/.config/redshift/hooks/backlight b/home/.config/redshift/hooks/backlight new file mode 100755 index 0000000000..23c076caad --- /dev/null +++ b/home/.config/redshift/hooks/backlight @@ -0,0 +1,57 @@ +#!/bin/sh +# redshift hook: dim the backlight along with the day/night cycle, via acpilight's +# xbacklight (see deps/acpilight, README). Portable across machines: acpilight +# enumerates both native ACPI panels (/sys/class/backlight/) and, once the +# ddcci-driver-linux kernel module is loaded, DDC/CI-controlled external monitors +# (/sys/class/backlight/ddcci*) under the same interface. +# +# Called by redshift as: backlight period-changed +set -u + +brightness_day=70 +brightness_transition=50 +brightness_night=30 +# redshift sets period to "none" (and fires this same period-changed hook) +# when disabled via SIGUSR1/the redshift-gtk tray toggle, alongside resetting +# color settings to neutral -- restore full brightness to match. +brightness_default=100 + +have() { command -v "$1" >/dev/null 2>&1; } + +have xbacklight || exit 0 + +case "${3:-}" in + daytime) brightness=$brightness_day ;; + transition) brightness=$brightness_transition ;; + night) brightness=$brightness_night ;; + none) brightness=$brightness_default ;; + *) exit 0 ;; +esac + +# Only look at actual display backlights, never acpilight's /sys/class/leds +# fallback (keyboard/capslock/etc. LEDs) -- on a machine with no backlight +# device at all, xbacklight's "first controller found" default would otherwise +# silently target one of those instead of a screen and fail or do the wrong +# thing. No backlight device present (e.g. ddcci-driver-linux not loaded +# yet): nothing to do. +backlight_ctrls=$(ls /sys/class/backlight 2>/dev/null) || exit 0 +[ -n "$backlight_ctrls" ] || exit 0 + +# Set every ddcci-exposed external monitor (a desktop may have several, +# independently registered per i2c bus -- see README); on machines with no +# ddcci device at all (e.g. a laptop's native panel only), fall through to +# xbacklight's own default controller, which is safe here since we already +# confirmed at least one /sys/class/backlight entry exists. Matched via +# `case`, not `grep ddcci*`, so it never risks shell pathname expansion +# against files in the current directory. +found_ddcci=0 +for name in $backlight_ctrls; do + case "$name" in + ddcci*) + xbacklight -ctrl "$name" -set "$brightness" + found_ddcci=1 + ;; + esac +done + +[ "$found_ddcci" -eq 1 ] || xbacklight -set "$brightness" diff --git a/home/.config/redshift/redshift.conf b/home/.config/redshift/redshift.conf new file mode 100644 index 0000000000..f34c7c6bf6 --- /dev/null +++ b/home/.config/redshift/redshift.conf @@ -0,0 +1,3 @@ +[redshift] +; slightly warm daytime color temperature (default neutral is 6500K) +temp-day=5500 diff --git a/home/.local/bin/xbacklight b/home/.local/bin/xbacklight new file mode 120000 index 0000000000..c3c889e711 --- /dev/null +++ b/home/.local/bin/xbacklight @@ -0,0 +1 @@ +../../../deps/acpilight/xbacklight \ No newline at end of file diff --git a/mk/build.mk b/mk/build.mk index cc64a487ca..4c7add1c98 100644 --- a/mk/build.mk +++ b/mk/build.mk @@ -45,6 +45,13 @@ $(xwinwrap_target): $(xwinwrap_head_file) $(MAKE) -C $(XWINWRAP_ROOT) xwinwrap: $(xwinwrap_target) +.PHONY: acpilight +acpilight_target = $(ACPILIGHT_ROOT)/xbacklight +$(eval $(call git_submodule,acpilight,$(ACPILIGHT_ROOT))) +# no build step - just ensure submodule +$(acpilight_target): $(acpilight_head_file) +acpilight: $(acpilight_target) + i3status-config-template := home/.config/i3status/config.template i3status-config := home/.config/i3status/config amd-cpu-temperature-path := /sys/class/hwmon/hwmon2/temp1_input @@ -292,6 +299,7 @@ logrotate: $(logrotate_target) TARGETS = \ mpv-mpris \ xwinwrap \ + acpilight \ ccls \ fzf \ fzy \ diff --git a/mk/variables.mk b/mk/variables.mk index d648befb4f..eed1134cc9 100644 --- a/mk/variables.mk +++ b/mk/variables.mk @@ -21,6 +21,7 @@ FONTS = home/.local/share/fonts MPV_MPRIS_ROOT = $(DEPS)/mpv-mpris XWINWRAP_ROOT = $(DEPS)/xwinwrap +ACPILIGHT_ROOT = $(DEPS)/acpilight CCLS_ROOT = $(DEPS)/ccls ATUIN_ROOT = $(DEPS)/atuin QMK_CLI_ROOT = $(DEPS)/qmk_cli