From db7e1155423982c862cfbdb2efa815352beb46da Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Thu, 14 Aug 2025 03:26:36 -0600 Subject: [PATCH] [NixOS] Force Brightness management to work --- dotfiles/config/xmonad/xmonad.hs | 4 +- dotfiles/lib/bin/brightness.hs | 164 +++++++++++++++++++++++++ dotfiles/lib/bin/brightness.sh | 44 ++----- dotfiles/lib/bin/brightness_manager.py | 89 -------------- 4 files changed, 178 insertions(+), 123 deletions(-) create mode 100755 dotfiles/lib/bin/brightness.hs delete mode 100755 dotfiles/lib/bin/brightness_manager.py diff --git a/dotfiles/config/xmonad/xmonad.hs b/dotfiles/config/xmonad/xmonad.hs index a01624e7..13821b35 100644 --- a/dotfiles/config/xmonad/xmonad.hs +++ b/dotfiles/config/xmonad/xmonad.hs @@ -1059,8 +1059,8 @@ addKeys conf@XConfig { modMask = modm } = , ((hyper .|. shiftMask, xK_q), spawn "toggle_mute_current_window.sh") , ((hctrl, xK_q), spawn "toggle_mute_current_window.sh only") - , ((0, xF86XK_MonBrightnessUp), spawn "brightness.sh +5") - , ((0, xF86XK_MonBrightnessDown), spawn "brightness.sh -5") + , ((0, xF86XK_MonBrightnessUp), spawn "brightness.sh up") + , ((0, xF86XK_MonBrightnessDown), spawn "brightness.sh down") ] ++ diff --git a/dotfiles/lib/bin/brightness.hs b/dotfiles/lib/bin/brightness.hs new file mode 100755 index 00000000..7d7b4b66 --- /dev/null +++ b/dotfiles/lib/bin/brightness.hs @@ -0,0 +1,164 @@ +#!/usr/bin/env runhaskell + +import System.Environment (getArgs) +import System.Process +import System.Exit +import Data.List (isPrefixOf, isSuffixOf) +import Data.Maybe (mapMaybe, fromMaybe) +import Text.Read (readMaybe) +import Control.Monad (forM, when) +import Data.Time.Clock +import Data.Time.Format +import System.IO + +-- Types +data Device = Device { deviceName :: String, deviceBrightness :: Int } + deriving (Show) + +data BrightnessCommand = Absolute Int | Increase Int | Decrease Int | Query + deriving (Show) + +-- Logging +logAction :: String -> IO () +logAction arg = do + time <- getCurrentTime + let timeStr = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" time + ppid <- readProcess "sh" ["-c", "ps -p $PPID -o comm= 2>/dev/null || echo unknown"] "" + pwd <- readProcess "pwd" [] "" + let logEntry = "[" ++ timeStr ++ "] Called with: '" ++ arg ++ "' | Parent: " ++ + filter (/= '\n') ppid ++ " | PWD: " ++ filter (/= '\n') pwd ++ "\n" + appendFile "/tmp/brightness.log" logEntry + +-- Parse command line argument +parseArg :: String -> BrightnessCommand +parseArg "" = Query +parseArg ('+':rest) = case readMaybe rest of + Just n -> Increase n + Nothing -> Query +parseArg ('-':rest) = case readMaybe rest of + Just n -> Decrease n + Nothing -> Query +parseArg s = case readMaybe s of + Just n -> Absolute n + Nothing -> Query + +-- Get list of backlight devices +getBacklightDevices :: IO [String] +getBacklightDevices = do + output <- readProcess "brightnessctl" ["--list"] "" + return $ mapMaybe extractDevice (lines output) + where + extractDevice line + | "Device '" `isInfixOf` line && "class 'backlight'" `isInfixOf` line = + let afterDevice = drop (length "Device '") line + deviceName = takeWhile (/= '\'') afterDevice + in if null deviceName then Nothing else Just deviceName + | otherwise = Nothing + +-- Get brightness percentage for a device +getDeviceBrightness :: String -> IO (Maybe Int) +getDeviceBrightness device = do + output <- readProcess "brightnessctl" ["-d", device] "" + return $ extractPercentage output + where + extractPercentage s = case dropWhile (/= '(') s of + ('(':rest) -> readMaybe (takeWhile (/= '%') rest) + _ -> Nothing + +-- Set brightness for a device +setDeviceBrightness :: String -> String -> IO () +setDeviceBrightness device cmd = do + _ <- readProcess "brightnessctl" ["-d", device, "set", cmd] "" + return () + +-- Build brightness command string +buildCommand :: BrightnessCommand -> String +buildCommand (Absolute n) = show n ++ "%" +buildCommand (Increase n) = show n ++ "%+" +buildCommand (Decrease n) = show n ++ "%-" +buildCommand Query = "" + +-- Apply brightness change to all devices +applyBrightness :: BrightnessCommand -> IO () +applyBrightness Query = return () +applyBrightness cmd = do + devices <- getBacklightDevices + let cmdStr = buildCommand cmd + + -- Log what we're about to do + appendFile "/tmp/brightness.log" $ + " Applying: " ++ cmdStr ++ " to devices: " ++ show devices ++ "\n" + + -- Apply to all devices + mapM_ (\dev -> setDeviceBrightness dev cmdStr) devices + + -- Sync devices if needed + when (length devices > 1) $ do + brightnesses <- forM devices $ \dev -> do + mbBright <- getDeviceBrightness dev + return (dev, mbBright) + + let validBrightnesses = [(d, b) | (d, Just b) <- brightnesses] + when (not $ null validBrightnesses) $ do + let values = map snd validBrightnesses + maxBright = maximum values + minBright = minimum values + + -- If devices are out of sync + when (maxBright /= minBright) $ do + -- Use minimum when brightness is low (15% or below) + let syncValue = if maxBright <= 15 then minBright else maxBright + -- Log sync decision for debugging + appendFile "/tmp/brightness.log" $ + " Syncing: max=" ++ show maxBright ++ "%, min=" ++ show minBright ++ + "%, using=" ++ show syncValue ++ "%\n" + mapM_ (\dev -> setDeviceBrightness dev (show syncValue ++ "%")) devices + +-- Get average brightness across all devices +getAverageBrightness :: IO Int +getAverageBrightness = do + devices <- getBacklightDevices + if null devices + then return 50 -- Default fallback + else do + brightnesses <- forM devices getDeviceBrightness + let validValues = [b | Just b <- brightnesses] + if null validValues + then return 50 + else return $ sum validValues `div` length validValues + +-- Send notification using rumno if available +sendNotification :: Int -> IO () +sendNotification brightness = do + rumnoExists <- (== ExitSuccess) <$> + rawSystem "sh" ["-c", "command -v rumno >/dev/null 2>&1"] + if rumnoExists + then do + _ <- readProcess "rumno" ["notify", "-b", show brightness] "" + return () + else putStrLn (show brightness) + +-- Check if a string contains another +isInfixOf :: String -> String -> Bool +isInfixOf needle haystack = any (isPrefixOf needle) (tails haystack) + where + tails [] = [[]] + tails xs@(_:xs') = xs : tails xs' + +main :: IO () +main = do + args <- getArgs + let arg = case args of + [] -> "" + (x:_) -> x + + -- Log the invocation + logAction arg + + -- Parse and apply command + let cmd = parseArg arg + applyBrightness cmd + + -- Get current brightness and notify + brightness <- getAverageBrightness + sendNotification brightness diff --git a/dotfiles/lib/bin/brightness.sh b/dotfiles/lib/bin/brightness.sh index fab48367..69f73bf7 100755 --- a/dotfiles/lib/bin/brightness.sh +++ b/dotfiles/lib/bin/brightness.sh @@ -3,6 +3,9 @@ # Get the brightness argument (default to empty string to just show current) ARG="${1:-}" +# Log the incoming arguments for debugging +echo "[$(date '+%Y-%m-%d %H:%M:%S')] brightness.sh called with: '$ARG' (all args: $@)" >> /tmp/brightness_bash.log + # Function to get current brightness percentage get_brightness_percentage() { # Get list of display backlight devices only (filter by class 'backlight') @@ -45,7 +48,13 @@ get_brightness_percentage() { # Apply brightness change if argument provided if [ -n "$ARG" ]; then # Determine if it's absolute or relative - if [[ "$ARG" == +* ]]; then + if [[ "$ARG" == "up" ]]; then + # Increase by 5% + BRIGHTNESS_CMD="5%+" + elif [[ "$ARG" == "down" ]]; then + # Decrease by 5% + BRIGHTNESS_CMD="5%-" + elif [[ "$ARG" == +* ]]; then # Relative increase (e.g., +5) BRIGHTNESS_CMD="${ARG:1}%+" elif [[ "$ARG" == -* ]]; then @@ -61,40 +70,11 @@ if [ -n "$ARG" ]; then DEVICES=$(brightnessctl --list 2>/dev/null | grep 'class.*backlight' | cut -d' ' -f2 | cut -d"'" -f2) if [ -n "$DEVICES" ]; then - # Apply to each device + # Apply to each device independently without syncing + # This allows each device to maintain its own brightness range for device in $DEVICES; do brightnessctl -d "$device" set "$BRIGHTNESS_CMD" >/dev/null 2>&1 done - - # Check if devices have different brightness values and sync them - device_count=$(echo "$DEVICES" | wc -w) - if [ "$device_count" -gt 1 ]; then - # Get all brightness values - max_brightness=0 - all_same=true - first_brightness="" - - for device in $DEVICES; do - brightness=$(brightnessctl -d "$device" 2>/dev/null | grep -oP '\d+%' | head -1 | tr -d '%') - if [ -n "$brightness" ]; then - if [ -z "$first_brightness" ]; then - first_brightness=$brightness - elif [ "$brightness" != "$first_brightness" ]; then - all_same=false - fi - if [ "$brightness" -gt "$max_brightness" ]; then - max_brightness=$brightness - fi - fi - done - - # If not all the same, set them all to the maximum - if [ "$all_same" = false ] && [ "$max_brightness" -gt 0 ]; then - for device in $DEVICES; do - brightnessctl -d "$device" set "${max_brightness}%" >/dev/null 2>&1 - done - fi - fi else # Fallback: just run brightnessctl without specifying device brightnessctl set "$BRIGHTNESS_CMD" >/dev/null 2>&1 diff --git a/dotfiles/lib/bin/brightness_manager.py b/dotfiles/lib/bin/brightness_manager.py deleted file mode 100755 index 9105d6a3..00000000 --- a/dotfiles/lib/bin/brightness_manager.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -import argparse -import os -import sys -import logging - -logger = logging.getLogger(__name__) - - -class BrightnessManager(object): - - @classmethod - def find_brightness(cls): - items_in_backlight_directory = os.listdir("/sys/class/backlight") - if len(items_in_backlight_directory) > 1: - logger.warning(f"More than one entry in the backlight directory {items_in_backlight_directory}") - return cls.from_path( - os.path.join("/sys/class/backlight", items_in_backlight_directory[0]) - ) - - @classmethod - def from_path(cls, path): - logger.warning(f"Using path {path}") - return cls( - set_brightness_filepath=os.path.join(path, "brightness"), - actual_brightness_filepath=os.path.join(path, "actual_brightness"), - max_brightness_filepath=os.path.join(path, "max_brightness"), - ) - - def __init__( - self, set_brightness_filepath, max_brightness_filepath, actual_brightness_filepath - ): - self.set_brightness_filepath = set_brightness_filepath - self.max_brightness_filepath = max_brightness_filepath - self.actual_brightness_filepath = actual_brightness_filepath - - @property - def current_brightness(self): - with open(self.actual_brightness_filepath) as fd: - return int(fd.read()) - - @property - def max_brightness(self): - with open(self.max_brightness_filepath) as fd: - return int(fd.read()) - - @current_brightness.setter - def current_brightness(self, brightness): - with open(self.set_brightness_filepath, 'w') as fd: - fd.write(str(brightness)) - - def increment_by_proportion(self, proportion): - new_brightness = self.current_brightness + int(self.max_brightness * proportion) - new_brightness = min(new_brightness, self.max_brightness) - self.current_brightness = new_brightness - - @property - def current_proportion(self): - return float(self.current_brightness) / self.max_brightness - - -def build_parser(): - parser = argparse.ArgumentParser( - description='Interact with macbook brightness', - ) - parser.add_argument( - "--change", "-c", - help="Change volume by the given percentage", - default=0 - ) - parser.add_argument( - "--print", "-p", dest="do_print", - action='store_true', - default=False - ) - return parser - - -if __name__ == '__main__': - args = build_parser().parse_args() - symlink_path = os.path.expanduser("~/.config/brightness_manager/symlink") - if os.path.exists(symlink_path): - symlink_path = os.readlink(symlink_path) - manager = BrightnessManager.from_path(symlink_path) - else: - manager = BrightnessManager.find_brightness() - manager.increment_by_proportion(float(args.change) / 100) - if args.do_print: - print(int(manager.current_proportion * 100))