Skip to content

Instantly share code, notes, and snippets.

@parichay28
Created March 14, 2025 21:55
Show Gist options
  • Select an option

  • Save parichay28/54f897bf0af50894070cd7b62a8aa167 to your computer and use it in GitHub Desktop.

Select an option

Save parichay28/54f897bf0af50894070cd7b62a8aa167 to your computer and use it in GitHub Desktop.
Hammerspoon config for fine grained brightness control without option+shift modifier
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
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
@parichay28

Copy link
Copy Markdown
Author

Supports

  • Multiple monitors with custom step size for each display vendor
  • Handling plugging & unplugging displays

Alternatives

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment