Created
May 28, 2025 10:38
-
-
Save ole/433e1a17eb6d6286187ea15b45bf92c4 to your computer and use it in GitHub Desktop.
OleScreenConfigNotifier: a Hammerspoon spoon for observing changes in the screen/display/monitor configuration, such as (dis-)connecting displays or resolution changes.
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
--- === ScreenConfigNotifier === | |
--- | |
--- Displays a local notification when the screen configuration changes. | |
--- | |
--- With some modifications, this can be used as the basis for triggering | |
--- other actions when the screen config changes. | |
--- Usage: | |
--- 1. Save this file as `OleScreenConfigNotifier.spoon/init.lua` in your Hammerspoon config folder. | |
--- 2. In your root `init.lua`, add these lines: | |
--- | |
--- hs.loadSpoon("OleScreenConfigNotifier") | |
--- spoon.OleScreenConfigNotifier:start() | |
local obj = {} | |
obj.__index = obj | |
-- Metadata | |
obj.name = "OleScreenConfigNotifier" | |
obj.version = "0.1" | |
obj.author = "Ole Begemann" | |
-- Internal variables | |
obj.watcher = nil | |
obj.currentScreenConfig = nil | |
--- ScreenConfigNotifier:start() | |
--- Method | |
--- Starts watching for screen configuration changes | |
--- | |
--- Parameters: | |
--- * None | |
--- | |
--- Returns: | |
--- * The ScreenConfigNotifier object | |
function obj:start() | |
if self.watcher then | |
self.watcher:stop() | |
end | |
-- Create a watcher that responds to both layout and active screen changes | |
self.watcher = hs.screen.watcher.newWithActiveScreen(function(isActiveScreenChange) | |
self:screenChanged(isActiveScreenChange) | |
end) | |
self.watcher:start() | |
-- Show initial notification on launch | |
-- This will implicitly store the initial screen config, which is why we don't | |
-- need an explicit `self.currentScreenConfig = self:getScreenInfo()` call here. | |
self:screenChanged(false) | |
return self | |
end | |
--- ScreenConfigNotifier:stop() | |
--- Method | |
--- Stops watching for screen configuration changes | |
--- | |
--- Parameters: | |
--- * None | |
--- | |
--- Returns: | |
--- * The ScreenConfigNotifier object | |
function obj:stop() | |
if self.watcher then | |
self.watcher:stop() | |
self.watcher = nil | |
end | |
return self | |
end | |
-- Callback that gets called when screen configuration changes | |
-- | |
-- When isActiveScreenChange is false, this means it's a layout change | |
-- When isActiveScreenChange is true, it's about the active screen changing | |
function obj:screenChanged(isActiveScreenChange) | |
local newScreenConfig = self:getScreenInfo() | |
-- Check if the configuration has actually changed | |
if self:isIdenticalConfig(self.currentScreenConfig, newScreenConfig) then | |
-- No change, don't show notification | |
-- But we still want to store the new config for the next comparison | |
self.currentScreenConfig = newScreenConfig | |
return | |
end | |
-- Update stored configuration | |
self.currentScreenConfig = newScreenConfig | |
local changeType = isActiveScreenChange and "Active screen changed" or "Screen layout changed" | |
local info = self:formatScreenInfo(newScreenConfig) | |
print(info) | |
-- Create a notification with screen information | |
local notification = hs.notify.new({ | |
title = changeType, | |
informativeText = info, | |
autoWithdraw = true, | |
hasActionButton = false | |
}) | |
notification:send() | |
end | |
--- ScreenConfigNotifier:getScreenInfo() | |
--- Method | |
--- Collect information about all screens as structured data | |
--- | |
--- Parameters: | |
--- * None | |
--- | |
--- Returns: | |
--- * An array of tables, each containing information about a screen: | |
--- * index - The screen's index number | |
--- * name - The screen's name or "Unknown" if not available | |
--- * uuid - The screen's UUID or "Unknown" if not available | |
--- * resolution - A table with: | |
--- * width - The screen's width in pixels | |
--- * height - The screen's height in pixels | |
--- * scale - The screen's scaling factor | |
function obj:getScreenInfo() | |
local screens = hs.screen.allScreens() | |
local screenData = {} | |
for i, screen in ipairs(screens) do | |
local mode = screen:currentMode() | |
screenData[i] = { | |
index = i, | |
name = screen:name() or "Unknown", | |
uuid = screen:getUUID() or "Unknown", | |
resolution = { | |
width = mode.w, | |
height = mode.h | |
}, | |
scale = mode.scale | |
} | |
end | |
return screenData | |
end | |
-- Format screen information as a string | |
function obj:formatScreenInfo(screens) | |
local info = "" | |
string.format("Found %d screen(s):\n", #screens) | |
for _, screen in ipairs(screens) do | |
local resolution = string.format("%d×%d", screen.resolution.width, screen.resolution.height) | |
info = info .. string.format( | |
"Screen %d/%d: %s\n%s @ %.1f×\nUUID: %s\n", | |
screen.index, #screens, screen.name, resolution, screen.scale, screen.uuid | |
) | |
end | |
return info | |
end | |
-- Compare two screen configurations to see if they're identical | |
function obj:isIdenticalConfig(config1, config2) | |
if not config1 and not config2 then | |
return true | |
elseif not config1 or not config2 then | |
return false | |
end | |
-- Check if the number of screens is the same | |
if #config1 ~= #config2 then | |
return false | |
end | |
-- Compare screens in order (position matters) | |
for i, screen1 in ipairs(config1) do | |
local screen2 = config2[i] | |
-- Check if UUIDs match | |
if screen1.uuid ~= screen2.uuid then | |
return false -- Different screen at this position | |
end | |
-- Check properties | |
if screen1.resolution.width ~= screen2.resolution.width or | |
screen1.resolution.height ~= screen2.resolution.height or | |
screen1.scale ~= screen2.scale then | |
return false -- Resolution or scale has changed | |
end | |
end | |
return true -- Configurations are identical | |
end | |
return obj |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment