diff --git a/bin/omarchy-hw-nightlight b/bin/omarchy-hw-nightlight new file mode 100755 index 00000000000..9748e1bb7ec --- /dev/null +++ b/bin/omarchy-hw-nightlight @@ -0,0 +1,35 @@ +#!/bin/bash + +# omarchy:summary=Detect whether the GPU can apply a nightlight color temperature. + +# hyprsunset sends its color temperature to Hyprland as a colour transform +# matrix over hyprland-ctm-control-v1, and aquamarine only submits a CTM on the +# DRM atomic path. A driver without atomic modesetting accepts the request and +# drops it, so hyprctl answers "ok", the temperature reads back, and the panel +# never changes — a silent no-op rather than an error. +# +# radeon is the one such driver still in use: it drives pre-GCN AMD cards +# (TeraScale and older, e.g. the Radeon HD 2400 in a 2007 iMac) and reports +# "crtc doesn't support CTM". Everything newer is on amdgpu and is atomic. +# +# Only answer no when every DRM card is on radeon. A machine that also has a +# non-radeon GPU may well be driving its display from that one, and hiding a +# working feature is worse than leaving a broken one visible. +drm_path="${OMARCHY_DRM_PATH:-/sys/class/drm}" + +cards=0 + +for card in "$drm_path"/card[0-9]*; do + name=${card##*/} + + # Connectors are exposed as card0-LVDS-1 alongside the card itself. + [[ $name == *-* ]] && continue + [[ -e $card/device/driver ]] || continue + + (( ++cards )) + [[ $(readlink -f "$card/device/driver") == */radeon ]] || exit 0 +done + +(( cards > 0 )) && exit 1 + +exit 0 diff --git a/bin/omarchy-toggle-nightlight b/bin/omarchy-toggle-nightlight index af717245fc1..38d030017b1 100755 --- a/bin/omarchy-toggle-nightlight +++ b/bin/omarchy-toggle-nightlight @@ -29,6 +29,22 @@ status_json() { '{enabled:$enabled, temperature:(($temp | tonumber?) // null)}' } +# Without a CTM-capable display pipeline hyprsunset still answers "ok" and +# reports the temperature back, so the retry loop below would run to completion +# and this command would claim success over a panel that never changed. Say so +# instead. The menu hides this row on such hardware; this covers the keybind, +# the CLI, and anything reading --status. +if ! omarchy-hw-nightlight; then + if [[ ${1:-} == "--status" ]]; then + jq -cn '{enabled:false, temperature:null}' + exit 0 + fi + + omarchy-notification-send -u normal "Nightlight unavailable" \ + "This GPU's display driver cannot apply a color temperature." + exit 1 +fi + if [[ ${1:-} == "--status" ]]; then status_json exit 0 diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 36518d09c4b..0cbc27da5ca 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -92,7 +92,7 @@ "trigger.toggle.crash-capture": {"icon":"󱚡","label":"Crash Capture","action":"omarchy-toggle-crash-capture"}, "trigger.toggle.screensaver": {"icon":"󱄄","label":"Screensaver","action":"omarchy-toggle-screensaver"}, "trigger.toggle.theme-sync": {"icon":"󰸌","label":"Herdr Theme Sync","when":"omarchy-theme-set-herdr-machines --available","action":"omarchy-toggle-theme-sync"}, - "trigger.toggle.nightlight": {"icon":"󰔎","label":"Nightlight","action":"omarchy-toggle-nightlight"}, + "trigger.toggle.nightlight": {"icon":"󰔎","label":"Nightlight","when":"omarchy-hw-nightlight","action":"omarchy-toggle-nightlight"}, "trigger.toggle.top-bar": {"icon":"󰍜","label":"Menu Bar","action":"omarchy-toggle-bar"}, "trigger.toggle.battery-percentage": {"icon":"󰁹","label":"Battery Percentage","when":"omarchy-hw-laptop","action":"omarchy-shell omarchy.power togglePercentage"}, "trigger.toggle.workspace-layout": {"icon":"󱂬","label":"Workspace Layout","action":"omarchy-hyprland-workspace-layout-toggle"}, diff --git a/install/hardware/fix-bcm43xx.sh b/install/hardware/fix-bcm43xx.sh index 0f9cf474a5b..52bee86f895 100644 --- a/install/hardware/fix-bcm43xx.sh +++ b/install/hardware/fix-bcm43xx.sh @@ -1,10 +1,11 @@ # Install Wi-Fi drivers for Broadcom chips found in some MacBooks, as well as other systems: # - BCM4360 (2013–2015 MacBooks) # - BCM4331 (2012, early 2013 MacBooks) +# - BCM4321 (2007–2008 iMacs and MacBooks; 4329 and 432a are the same chip) pci_info=$(lspci -nn) -if (echo "$pci_info" | grep -q "14e4:43a0" || echo "$pci_info" | grep -q "14e4:4331"); then - echo "BCM4360 / BCM4331 detected" +if echo "$pci_info" | grep -qE "14e4:(43a0|4331|4328|4329|432a)"; then + echo "Broadcom BCM4360 / BCM4331 / BCM4321 detected" omarchy-pkg-add broadcom-wl-dkms fi diff --git a/install/hardware/vulkan.sh b/install/hardware/vulkan.sh index a2447d46ffa..d93b0ca5c3e 100644 --- a/install/hardware/vulkan.sh +++ b/install/hardware/vulkan.sh @@ -9,8 +9,39 @@ declare -A VULKAN_DRIVERS=( PACKAGES=() +# RADV only drives GPUs bound to amdgpu. Pre-GCN cards (TeraScale and older, +# e.g. the Radeon HD 2400 in a 2007 iMac) stay on the radeon driver, where +# vulkan-radeon still installs radeon_icd.json but every device enumeration +# fails with VK_ERROR_INCOMPATIBLE_DRIVER. That stray ICD makes +# omarchy-hw-vulkan answer yes on a machine with no working Vulkan. +# +# Skip only on radeon: an installer booted with nomodeset binds no driver at +# all, which says nothing about the installed system. +amd_gpu_only_on_radeon() { + local device driver + local on_radeon=false + local pci_devices_path="${OMARCHY_PCI_DEVICES_PATH:-/sys/bus/pci/devices}" + + for device in "$pci_devices_path"/*; do + [[ -e $device/vendor ]] || continue + [[ $(< "$device/vendor") == "0x1002" ]] || continue + [[ $(< "$device/class") == 0x03* ]] || continue + + driver=$(readlink -f "$device/driver") + [[ $driver == */amdgpu ]] && return 1 + [[ $driver == */radeon ]] && on_radeon=true + done + + [[ $on_radeon == true ]] +} + for vendor in "${!VULKAN_DRIVERS[@]}"; do if lspci | grep -iE "(VGA|Display).*$vendor" > /dev/null; then + if [[ $vendor == "AMD" ]] && amd_gpu_only_on_radeon; then + echo "Skipping vulkan-radeon: AMD GPU is on the radeon driver, which RADV cannot drive" + continue + fi + PACKAGES+=("${VULKAN_DRIVERS[$vendor]}") fi done diff --git a/test/shell.d/broadcom-wifi-test.sh b/test/shell.d/broadcom-wifi-test.sh new file mode 100644 index 00000000000..06d0cb3dc98 --- /dev/null +++ b/test/shell.d/broadcom-wifi-test.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +fix_bcm="$ROOT/install/hardware/fix-bcm43xx.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +mkdir -p "$stub_bin" + +# Real lspci keeps printing well past any match, so a grep -q consumer can be +# killed by SIGPIPE; keep the stub chatty for the same reason as the T2 test. +cat >"$stub_bin/lspci" <<'SH' +#!/bin/bash +[[ -n ${STUB_WIFI_ID:-} ]] && + echo "04:00.0 Network controller [0280]: Broadcom Inc. and subsidiaries Wireless [$STUB_WIFI_ID] (rev 03)" +for _ in {1..4096}; do + echo '02:00.0 Host bridge [0600]: Filler Device [ffff:0000]' +done +SH +chmod +x "$stub_bin/lspci" + +installs_driver_for() { + local wifi_id=${1:-} + local calls="$test_tmp/calls-$RANDOM" + + PATH="$stub_bin:$PATH" \ + STUB_WIFI_ID="$wifi_id" \ + OMARCHY_TEST_CALLS="$calls" \ + "$BASH" -c ' + omarchy-pkg-add() { printf "%s\n" "$@" >>"$OMARCHY_TEST_CALLS"; } + source "$1" + ' _ "$fix_bcm" >/dev/null 2>&1 + + [[ -f $calls ]] && grep -qx 'broadcom-wl-dkms' "$calls" +} + +# BCM4321 is the 2007-2008 iMac/MacBook card. b43 can drive it, but Omarchy +# standardized on the wl driver, which covers it too. +for id in 14e4:4328 14e4:4329 14e4:432a; do + installs_driver_for "$id" || + fail "BCM4321 ($id) gets broadcom-wl-dkms" +done +pass "BCM4321 device ids pull in broadcom-wl-dkms" + +# Regression guard: the ids that already worked must keep working. +installs_driver_for 14e4:43a0 || fail "BCM4360 still gets broadcom-wl-dkms" +installs_driver_for 14e4:4331 || fail "BCM4331 still gets broadcom-wl-dkms" +pass "BCM4360 and BCM4331 keep their driver" + +# A Broadcom card brcmfmac drives (BCM4350) must not drag wl in. +! installs_driver_for 14e4:43a3 || + fail "a brcmfmac Broadcom id does not install broadcom-wl-dkms" +! installs_driver_for || + fail "a machine with no Broadcom wireless installs nothing" +pass "non-matching hardware installs no Broadcom driver" diff --git a/test/shell.d/nightlight-hardware-test.sh b/test/shell.d/nightlight-hardware-test.sh new file mode 100644 index 00000000000..88f7a0d319c --- /dev/null +++ b/test/shell.d/nightlight-hardware-test.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +hw_nightlight="$ROOT/bin/omarchy-hw-nightlight" +toggle="$ROOT/bin/omarchy-toggle-nightlight" +menu="$ROOT/default/omarchy/omarchy-menu.jsonc" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +# A /sys/class/drm shaped tree: one directory per card bound to the named +# driver, plus the connector directories the real thing interleaves with them. +make_drm_tree() { + local tree="$test_tmp/drm-$RANDOM" + local index=0 + local driver + + mkdir -p "$tree/drivers" + + for driver in "$@"; do + mkdir -p "$tree/card$index/device" "$tree/drivers/$driver" + ln -s "../../drivers/$driver" "$tree/card$index/device/driver" + # Connectors hang off the same directory and must not be counted as cards. + mkdir -p "$tree/card$index-LVDS-1" "$tree/card$index-DVI-I-1" + (( ++index )) + done + + echo "$tree" +} + +supports_nightlight() { + OMARCHY_DRM_PATH="$1" "$BASH" "$hw_nightlight" +} + +# Pre-GCN AMD: aquamarine's legacy DRM path never submits a CTM, so hyprsunset +# is a silent no-op here. +! supports_nightlight "$(make_drm_tree radeon)" || + fail "a radeon-only machine reports no nightlight support" +pass "radeon-only hardware reports no nightlight support" + +# Everything atomic must keep the feature. +supports_nightlight "$(make_drm_tree amdgpu)" || + fail "an amdgpu machine still supports nightlight" +supports_nightlight "$(make_drm_tree i915)" || + fail "an Intel machine still supports nightlight" +pass "atomic drivers keep nightlight" + +# Conservative on mixed hardware: the display may be driven by the other GPU, +# and hiding a working feature is worse than leaving a broken one visible. +supports_nightlight "$(make_drm_tree radeon i915)" || + fail "a machine with both radeon and an atomic GPU keeps nightlight" +pass "mixed radeon plus atomic hardware keeps nightlight" + +# Nothing to go on is not evidence of breakage. +supports_nightlight "$(make_drm_tree)" || + fail "a tree with no DRM cards does not claim nightlight is broken" +pass "an empty DRM tree keeps nightlight" + +# The guard has to be wired up in both places, or the silent no-op comes back. +grep -Fq '"when":"omarchy-hw-nightlight"' "$menu" || + fail "the menu hides the Nightlight row on hardware that cannot do it" +grep -Fq 'if ! omarchy-hw-nightlight; then' "$toggle" || + fail "omarchy-toggle-nightlight refuses instead of reporting a false success" +pass "the nightlight guard is wired into the menu and the toggle" diff --git a/test/shell.d/nightlight-test.sh b/test/shell.d/nightlight-test.sh index 43f3ef4f153..7c61bd49693 100644 --- a/test/shell.d/nightlight-test.sh +++ b/test/shell.d/nightlight-test.sh @@ -47,12 +47,28 @@ cat >"$TMPDIR/bin/omarchy-shell" <<'SH' printf '%s\n' "$*" >>"$OMARCHY_SHELL_LOG" SH -chmod +x "$TMPDIR/bin/hyprctl" "$TMPDIR/bin/pgrep" "$TMPDIR/bin/omarchy-shell" +# Hardware that cannot apply a CTM is the interesting second case, so make the +# capability probe switchable rather than assuming it always answers yes. +cat >"$TMPDIR/bin/omarchy-hw-nightlight" <<'SH' +#!/bin/bash +exit "${NIGHTLIGHT_SUPPORTED:-0}" +SH + +cat >"$TMPDIR/bin/omarchy-notification-send" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >>"$NOTIFICATION_LOG" +SH + +NOTIFICATIONS="$TMPDIR/notifications" + +chmod +x "$TMPDIR/bin/hyprctl" "$TMPDIR/bin/pgrep" "$TMPDIR/bin/omarchy-shell" \ + "$TMPDIR/bin/omarchy-hw-nightlight" "$TMPDIR/bin/omarchy-notification-send" nightlight_cli() { PATH="$TMPDIR/bin:$PATH" \ HYPRSUNSET_STATE="$STATE" \ OMARCHY_SHELL_LOG="$SHELL_LOG" \ + NOTIFICATION_LOG="$NOTIFICATIONS" \ "$ROOT/bin/omarchy-toggle-nightlight" "$@" } @@ -90,3 +106,21 @@ if rg -q 'omarchy.indicators' "$ROOT/bin/omarchy-toggle-nightlight"; then fail "nightlight toggle leaves indicator refresh to the nightlight service" fi pass "nightlight toggle leaves indicator refresh to the nightlight service" + +# On a display pipeline with no CTM support hyprsunset still answers "ok" and +# echoes the temperature back, so the toggle has to refuse up front rather than +# run its retry loop and claim a success the panel never showed. +printf '6500\n' >"$STATE" +: >"$NOTIFICATIONS" + +NIGHTLIGHT_SUPPORTED=1 nightlight_cli >/dev/null 2>&1 && + fail "nightlight toggle exits non-zero on hardware that cannot apply a temperature" +[[ $(<"$STATE") == 6500 ]] || + fail "nightlight toggle leaves the temperature alone on unsupported hardware" +grep -Fq 'Nightlight unavailable' "$NOTIFICATIONS" || + fail "nightlight toggle tells the user why it did nothing" +pass "nightlight toggle refuses on hardware that cannot apply a temperature" + +[[ $(NIGHTLIGHT_SUPPORTED=1 nightlight_status 4000 | jq -r .enabled) == "false" ]] || + fail "nightlight status reports disabled on unsupported hardware" +pass "nightlight status reports disabled on unsupported hardware" diff --git a/test/shell.d/vulkan-hardware-test.sh b/test/shell.d/vulkan-hardware-test.sh new file mode 100644 index 00000000000..4dc4c94fa62 --- /dev/null +++ b/test/shell.d/vulkan-hardware-test.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +vulkan="$ROOT/install/hardware/vulkan.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +mkdir -p "$stub_bin" + +# lspci only has to name the vendor on a VGA line; the driver question is +# answered from sysfs, which is what this script actually gates on. +cat >"$stub_bin/lspci" <<'SH' +#!/bin/bash +echo "01:00.0 VGA compatible controller: ${STUB_GPU_VENDOR:-Advanced Micro Devices, Inc. [AMD/ATI]} RV610" +SH +chmod +x "$stub_bin/lspci" + +# A GPU claimed by $1 ("radeon" or "amdgpu"), shaped like /sys/bus/pci/devices. +make_pci_tree() { + local driver=$1 + local vendor=${2:-0x1002} + local tree="$test_tmp/pci-$driver-$RANDOM" + + mkdir -p "$tree/0000:01:00.0" "$tree/drivers/$driver" + echo "$vendor" >"$tree/0000:01:00.0/vendor" + echo "0x030000" >"$tree/0000:01:00.0/class" + ln -s "../drivers/$driver" "$tree/0000:01:00.0/driver" + + echo "$tree" +} + +# Sourcing vulkan.sh with omarchy-pkg-add stubbed records what it would install. +# The packages go to a file rather than stdout: the script also explains itself +# on stdout, and those messages name the very packages being asserted on. +installed_packages() { + local pci_path=$1 + local calls="$test_tmp/calls-$RANDOM" + + # "$BASH", not "bash": vulkan.sh needs an associative array, and a stray + # bash 3.2 earlier in PATH would silently drop the AMD entry instead. + PATH="$stub_bin:$PATH" \ + OMARCHY_PCI_DEVICES_PATH="$pci_path" \ + OMARCHY_TEST_CALLS="$calls" \ + "$BASH" -c ' + omarchy-pkg-add() { printf "%s\n" "$@" >>"$OMARCHY_TEST_CALLS"; } + source "$1" + ' _ "$vulkan" >/dev/null 2>&1 + + [[ -f $calls ]] && cat "$calls" || true +} + +# Pre-GCN: radeon-driven AMD GPUs cannot be used by RADV, and the ICD that +# vulkan-radeon drops in would otherwise make omarchy-hw-vulkan answer yes. +pre_gcn=$(make_pci_tree radeon) +result=$(installed_packages "$pre_gcn") +[[ $result != *vulkan-radeon* ]] || + fail "vulkan-radeon is skipped on a pre-GCN AMD GPU bound to radeon (got: $result)" +pass "pre-GCN AMD GPUs on the radeon driver do not get vulkan-radeon" + +# GCN and newer still must, or every supported AMD machine loses Vulkan. +gcn=$(make_pci_tree amdgpu) +result=$(installed_packages "$gcn") +[[ $result == *vulkan-radeon* ]] || + fail "vulkan-radeon is still installed for an amdgpu-bound GPU (got: $result)" +pass "AMD GPUs on the amdgpu driver still get vulkan-radeon" + +# A GPU with no bound driver (an installer booted with nomodeset) says nothing +# about the installed system, so it keeps vulkan-radeon as before. +unbound="$test_tmp/pci-unbound" +mkdir -p "$unbound/0000:01:00.0" +echo "0x1002" >"$unbound/0000:01:00.0/vendor" +echo "0x030000" >"$unbound/0000:01:00.0/class" +result=$(installed_packages "$unbound") +[[ $result == *vulkan-radeon* ]] || + fail "vulkan-radeon is still installed when no driver is bound to the AMD GPU (got: $result)" +pass "an AMD GPU with no bound driver still gets vulkan-radeon" + +# The AMD guard must not touch the other vendors' drivers. +result=$(STUB_GPU_VENDOR="Intel Corporation" installed_packages "$pre_gcn") +[[ $result == *vulkan-intel* ]] || + fail "Intel GPUs still get vulkan-intel regardless of the AMD guard (got: $result)" +pass "the AMD guard leaves vulkan-intel selection alone"