Created
March 14, 2025 21:55
-
-
Save parichay28/54f897bf0af50894070cd7b62a8aa167 to your computer and use it in GitHub Desktop.
Hammerspoon config for fine grained brightness control without option+shift modifier
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local M = {} | |
| -- Constants | |
| local DEFAULT_BRIGHTNESS_STEP = 2 | |
| -- Module level variables for state management | |
| local systemKeyTap = nil | |
| local screenWatcher = nil | |
| local lastDisplayCount = #hs.screen.allScreens() -- Move this to module level | |
| local restartEventTapDelay = 3 -- In seconds | |
| -- Map of screen name patterns to brightness steps | |
| local SCREEN_BRIGHTNESS_STEPS = { | |
| ["built%-in"] = 2, -- Built-in display | |
| ["dell"] = 2, -- Dell monitors | |
| ["lg"] = 2, -- LG monitors | |
| ["asus"] = 2, -- ASUS monitors | |
| } | |
| -- Function to get brightness step for screen | |
| local function getBrightnessStep(screen) | |
| local screenName = screen:name():lower() | |
| for pattern, step in pairs(SCREEN_BRIGHTNESS_STEPS) do | |
| local lowerPattern = pattern:lower() | |
| if screenName:match(lowerPattern) then | |
| return step | |
| end | |
| end | |
| return DEFAULT_BRIGHTNESS_STEP | |
| end | |
| -- Function to round to nearest step - hs.brightness.set() doesn't always respect the value sent as arg | |
| local function roundToStep(value, step) | |
| return math.floor(value / step + 0.5) * step | |
| end | |
| -- Function to check if a screen is the built-in display | |
| local function isBuiltInDisplay(screen) | |
| local name = screen:name():lower() | |
| return not not (name:match("built%-in")) | |
| end | |
| -- Function to get active screen and check brightness support | |
| local function getActiveScreen() | |
| -- Try to get the screen with mouse first | |
| local mouseScreen = hs.mouse.getCurrentScreen() | |
| if mouseScreen then | |
| if mouseScreen:getBrightness() then | |
| return mouseScreen | |
| end | |
| end | |
| -- If no mouse screen, try main screen (screen with focused window) | |
| local mainScreen = hs.screen.mainScreen() | |
| if mainScreen and mainScreen:getBrightness() then | |
| return mainScreen | |
| end | |
| return nil | |
| end | |
| local function handleBrightnessKey(event) | |
| local systemKey = event:systemKey() | |
| -- Only handle brightness keys and ensure it's a keydown event | |
| if (systemKey.key == "BRIGHTNESS_DOWN" or systemKey.key == "BRIGHTNESS_UP") and systemKey.down then | |
| local activeScreen = getActiveScreen() | |
| if not activeScreen then | |
| return false | |
| end | |
| local brightnessStep = getBrightnessStep(activeScreen) | |
| -- Always round current brightness to nearest step for consistent step size | |
| local currentBrightness = hs.brightness.get() | |
| local currentRoundedBrightness = roundToStep(currentBrightness, brightnessStep) | |
| local newBrightness | |
| if systemKey.key == "BRIGHTNESS_UP" then | |
| if currentRoundedBrightness == 0 then | |
| newBrightness = brightnessStep | |
| else | |
| newBrightness = math.min(currentRoundedBrightness + brightnessStep, 100) | |
| end | |
| else | |
| if currentRoundedBrightness <= brightnessStep then | |
| newBrightness = 0 | |
| else | |
| newBrightness = currentRoundedBrightness - brightnessStep | |
| end | |
| end | |
| print(string.format("Setting brightness for %s: %d -> %d", activeScreen:name(), currentRoundedBrightness, newBrightness)) | |
| local success = hs.brightness.set(newBrightness) | |
| -- Verify the change and round it to check if it matches our expected step | |
| local currentBrightnessToVerify = hs.brightness.get() | |
| local currentRoundedBrightnessToVerify = roundToStep(currentBrightnessToVerify, brightnessStep) | |
| -- If the brightness didn't set correctly, try to force it | |
| if currentRoundedBrightnessToVerify ~= newBrightness then | |
| hs.brightness.set(newBrightness) | |
| end | |
| return true -- Prevent original event from being processed | |
| end | |
| return false | |
| end | |
| -- Function to restart event tap | |
| local function restartEventTap() | |
| if systemKeyTap then | |
| systemKeyTap:stop() | |
| print("Event tap stopped") | |
| end | |
| systemKeyTap:start() | |
| print("Event tap restarted") | |
| end | |
| -- Function to handle screen changes | |
| local function handleScreenChange() | |
| local currentDisplayCount = #hs.screen.allScreens() | |
| if currentDisplayCount ~= lastDisplayCount then | |
| print(string.format("\nDisplay count changed: %d -> %d", lastDisplayCount, currentDisplayCount)) | |
| lastDisplayCount = currentDisplayCount | |
| -- Add a small delay to ensure system has finished display configuration | |
| hs.timer.doAfter(restartEventTapDelay, function() | |
| -- Check active screen and restart event tap | |
| local activeScreen = getActiveScreen() | |
| if activeScreen then | |
| restartEventTap() | |
| end | |
| end) | |
| end | |
| end | |
| -- Function to start monitoring brightness keys | |
| function M.start() | |
| print("Starting brightness control...") | |
| -- Create event tap if it doesn't exist | |
| if not systemKeyTap then | |
| systemKeyTap = hs.eventtap.new({hs.eventtap.event.types.systemDefined}, handleBrightnessKey) | |
| end | |
| -- Create screen watcher if it doesn't exist | |
| if not screenWatcher then | |
| screenWatcher = hs.screen.watcher.new(handleScreenChange) | |
| end | |
| -- Start the event tap and screen watcher | |
| systemKeyTap:start() | |
| screenWatcher:start() | |
| print("Event tap and screen watcher started") | |
| -- Return cleanup function | |
| return function() | |
| if systemKeyTap then | |
| systemKeyTap:stop() | |
| systemKeyTap = nil | |
| print("Event tap stopped and cleaned up") | |
| end | |
| if screenWatcher then | |
| screenWatcher:stop() | |
| screenWatcher = nil | |
| print("Screen watcher stopped and cleaned up") | |
| end | |
| end | |
| end | |
| return M |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| print("Loading Hammerspoon configuration...") | |
| -- Load brightness control module | |
| local brightnessControl = require('brightness_control') | |
| -- Start brightness control and store cleanup function | |
| local cleanupBrightnessControl = brightnessControl.start() | |
| -- Cleanup on script reload | |
| hs.shutdownCallback = function() | |
| if cleanupBrightnessControl then | |
| cleanupBrightnessControl() | |
| end | |
| end |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Supports
Alternatives
Karabiner-elements(see config)