Apparently, Apple changed the design of com.apple.windowserver.plist and its information about screen sizes and configurations in macOS 13.x (Ventura). With some help from the getScreenResolution() discussion to get the correct plist file. I wrote a displayProperties() function returning properties of all displays connected.
As a result you get a list of display properties, including OriginX and OriginY containing their position relative to the main display. (Whereas the main display has OriginX and OriginY set to 0.0.)
Each display property item holds the following properties:
High: Height of the display in points (i.e virtual pixels)
Wide: Width of the display in points
Scale: Resolution of the display (calculate hardware pixels with points Ă— scale)
OriginX: Horizontal position of the display relative to the main display in points
OriginY: Vertical position of the display relative to the main display in points
Hz: Refresh rate of the display in Hz
And some properties with unkonwn meaning:
isLink
Depth: Maybe color depth?
IsVRR: Maybe true for internal displays?
Here is the function:
set myDisplayProperties to displayProperties()
on displayProperties()
-- Moritz Zimmer, moritz@zimmer.partners, 31.7.2023
set monitorConfigs to {}
set windowserverPrefFilePath to do shell script "find " & (POSIX path of ((path to preferences from user domain as Unicode text) & "ByHost:")) & " -name 'com.apple.windowserver*'"
tell application "System Events"
set myDisplayConfigs to DisplayConfig of item 1 of Configs of (item 2 of (get value of property list items of property list file windowserverPrefFilePath))
-- set logVariable to toString(myDisplayConfigs) of me
repeat with myDisplayConfigCounter from 1 to length of myDisplayConfigs
copy (CurrentInfo of item myDisplayConfigCounter of myDisplayConfigs) to end of monitorConfigs
end repeat
end tell
return monitorConfigs
end displayProperties
Please note, that the design of com.apple.windowserver.plist might change in future macOS releases and you probably need to start over reverse engineering its JSON structure to adapt the reference in item 1 of Configs of (item 2 of (get value of property list items of property list file windowserverPrefFilePath) . (You could use mklement0’s logging functions to output the content of the resulting myDisplayConfigs variable.)
I reverse engineered Window Server’s Plist even further and happen to learn, that it might be saved to System’s Library in some circumstances and hold not only one, but many screen configurations. (It’s a probably a store for all screens and their configurations ever connected to a Mac.) I already have a new version of the script above taking other plist file locations into account and exposing all the screen configurations and their screen sizes and properties. I have to debug and optimize it a little further. I keep you posted!
So, here we go; this is am extended version of the script reading display properties from Windows Server’s plist. It first tries to read the System’s configurations, then the User’s.
-- getDisplayConfigs() by moritz@zimmer.partners
-- License: CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
set displayConfigs to getDisplayConfigs()
return displayConfigs
on getDisplayConfigs()
-- Moritz Zimmer, moritz@zimmer.partners, 31.7.2023
set monitorConfigs to {}
set windowserverPrefFilePath to do shell script "find " & (POSIX path of (path to preferences from local domain as Unicode text)) & " -name 'com.apple.windowserver*'"
if windowserverPrefFilePath is missing value or windowserverPrefFilePath is "" then
set windowserverPrefFilePath to do shell script "find " & (POSIX path of ((path to preferences from user domain as Unicode text) & "ByHost:")) & " -name 'com.apple.windowserver*'"
set rootNodeWindowserverPref to "DisplaySets"
else
set rootNodeWindowserverPref to "DisplayAnyUserSets"
end if
-- return "find " & (POSIX path of ((path to preferences from user domain as Unicode text) & "ByHost:")) & " -name 'com.apple.windowserver*'"
if windowserverPrefFilePath is not missing value and windowserverPrefFilePath is not "" then
tell application "System Events"
set myWindowServerPref to (property list items of (property list items of ((property list items of property list file windowserverPrefFilePath) whose name is rootNodeWindowserverPref) whose name is "Configs"))
try
set myConfigs to value of item 1 of item 1 of myWindowServerPref
repeat with myConfig in myConfigs
set myDiplayConfigs to {}
repeat with myDisplayConfig in DisplayConfig of myConfig
copy (CurrentInfo of myDisplayConfig) to end of myDiplayConfigs
end repeat
-- return myDiplayConfigs
copy myDiplayConfigs to end of monitorConfigs
end repeat
on error
-- WIP: Add other heuristics to get screen properties
end try
end tell
else
-- WIP: Add other heuristics to get screen properties
end if
return monitorConfigs
end getDisplayConfigs
As you probably guessed reading the on error clause, I might add other methods to get screen resolutions if we fail to read them from Window Server config later on.
Wow! Digging though Apple’s documentation I found NSScreen exposing all current screens (desktops) and their resolutions and other information including how they are position in relation to the main screen. Here’s the AppleScript using NSScreen to read all connected screen’s resolution and other properties and return them as list of records:
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use framework "AppKit"
set screenRecords to screenRecords(false)
return screenRecords
on screenRecords(mainScreenOnly)
-- Get the array of screens using Objective-C
set screens to current application's NSScreen's screens()
-- Convert the NSScreen objects to AppleScript records
set screenRecords to {}
repeat with screen in screens
set screenBounds to screen's frame()
set screenVisibleBounds to screen's visibleFrame()
set screenName to screen's localizedName() as string
set screenBackingScaleFactor to screen's backingScaleFactor() as string
if (item 1 of item 1 of screenBounds is 0 and item 2 of item 1 of screenBounds is 0) then
set screenRecord to {name:screenName, frame:{|left|:item 1 of item 1 of screenBounds, top:item 2 of item 1 of screenBounds, width:item 1 of item 2 of screenBounds, height:item 2 of item 2 of screenBounds}, visibleFrame:{|left|:item 1 of item 1 of screenVisibleBounds, top:item 2 of item 1 of screenVisibleBounds, width:item 1 of item 2 of screenVisibleBounds, height:item 2 of item 2 of screenVisibleBounds}, scaleFactor:screenBackingScaleFactor, isMainScreen:false}
if mainScreenOnly is true then
return screenRecord
end if
else
set screenRecord to {name:screenName, frame:{|left|:item 1 of item 1 of screenBounds, top:item 2 of item 1 of screenBounds, width:item 1 of item 2 of screenBounds, height:item 2 of item 2 of screenBounds}, visibleFrame:{|left|:item 1 of item 1 of screenVisibleBounds, top:item 2 of item 1 of screenVisibleBounds, width:item 1 of item 2 of screenVisibleBounds, height:item 2 of item 2 of screenVisibleBounds}, scaleFactor:screenBackingScaleFactor, isMainScreen:false}
end if
set end of screenRecords to screenRecord
end repeat
return screenRecords
end screenRecords
Every resulting item holds the following properties (i.e. that you can retrieve by get height of frame of item 1 of screenRecords etc.):
frame (resolution of the display)
top
left
height
width
visibleFrame (area available for apps)
top
left
height
width
isMainScreen
name
scaleFactor (eg. 2.0 for high resolution displays and 1.0 for normal ones)
moritzz. Thanks for the NSScreen script, which works well.
For forum members not familiar with NSScreen, it’s probably worth noting that the point of origin is the lower-left corner of the screen, and that’s why your script returns 0 for the top of the visible screen on my computer. FWIW, the following converts the point of origin of the main screen to the upper-left corner.
use framework "AppKit"
use framework "Foundation"
use scripting additions
set visibleDisplayPositionSize to getVisibleDisplayPositionSize() --> {0, 25, 1920, 1055}
on getVisibleDisplayPositionSize()
set theScreen to current application's NSScreen's mainScreen()
set {{x1, y1}, {w1, h1}} to theScreen's frame()
set {{x2, y2}, {w2, h2}} to theScreen's visibleFrame()
return {x2 as integer, (h1 - h2 - y2) as integer, w2 as integer, h2 as integer}
end getVisibleDisplayPositionSize
@peavine for you input.Your right! I forgot this. I’ll update my script as soon as I have my office setup with more than one display connected available. I will update my function to automatically transform all x and y coordinates from bottom to top.