Skip to content

Instantly share code, notes, and snippets.

@joevt
Last active March 10, 2024 08:39
Show Gist options
  • Save joevt/0c75b42171b3fb1a5248b4e2bee8e4d0 to your computer and use it in GitHub Desktop.
Save joevt/0c75b42171b3fb1a5248b4e2bee8e4d0 to your computer and use it in GitHub Desktop.
perl script parses ioreg output, can output JSON, dumps M1 Mac display timings.
#!/bin/perl
# by joevt Nov 18, 2021
use 5.010;
use strict;
#use warnings;
use Data::Dumper qw(Dumper);
use JSON::PP;
my $parseall = 1;
my $nodeindent = 0;
my $nodename = "";
my $thepath = "";
my $nodeid = "";
my $nodeflags = "";
my $classname = "";
my $propertyndx = 0;
my $stringndx = 0;
my $fieldndx = 0;
my $fieldsndx = 0;
my $itemsndx = 0;
my $itemndx = 0;
my @property = [];
my @value = [];
my @string = [];
my @field = [];
my @fields = [];
my @items = [];
my @item = [];
my $regex = qr !(?x) # ignore white space in regular expressions (means all spaces, tabs, and newlines need to be escaped)
(?m) # ^ matches line instead of start of string
#(?i) # ^ case insensitive
#(?p) # Preserve the string matched such that ${^PREMATCH}, ${^MATCH}, and ${^POSTMATCH} are available for use after matching.
(?{
$stringndx = 0;
$fieldndx = 0;
$fieldsndx = 0;
$itemsndx = 0;
$itemndx = 0;
})
(?(DEFINE)(?'indent'^[|\ ]*+))
(?(DEFINE)(?'nodehead'(?P<nodeindent>(?&indent))\+\-o\ (?P<nodename>.*?)\ \ <class\ (?P<classname>[^,]+),\ id\ (?P<nodeid>[^,]+),\ (?P<nodeflags>[^>]+)>\n(?{$nodename=$+{nodename}; $nodeindent=(length $+{nodeindent})/2; $classname=$+{classname}; $nodeid=$+{nodeid}; $nodeflags=$+{nodeflags}})))
(?(DEFINE)(?'nodeproperties'((?&indent)\{\n(?{$propertyndx=0;})(?&property)++(?&indent)\}\n|)))
(?(DEFINE)(?'property'(?&indent)(?&string)(?{$property[$propertyndx]=$string[$stringndx-1];})\ =\ (?P<value>(?&item))(?{$value[$propertyndx++]=$+{value};}).*\n))
(?(DEFINE)(?'node'(?&nodehead)(?&nodeproperties)))
(?(DEFINE)(?'string'"(?P<stringx>[^"]*+)"(?{$string[$stringndx++]=$+{stringx}})))
(?(DEFINE)(?'number'\d++))
(?(DEFINE)(?'boolean'(?:No|Yes)))
(?(DEFINE)(?'data'<[0-9a-f]*+>))
(?(DEFINE)(?'datahex'(?:\n(?&indent)[0-9A-F]++:(?:\ [0-9A-F]{2})++.*)++))
(?(DEFINE)(?'field'(?&string)(?{$field[$fieldndx++]=$string[$stringndx-1]})=(?&item)))
(?(DEFINE)(?'dictionary'\{(?P<fields>(?:((?&field),)*+(?&field))|)(?{$fields[$fieldsndx++]=$+{fields}})\}))
(?(DEFINE)(?'array'\((?P<items>(?:((?&item),)*+(?&item))|)(?{$items[$itemsndx++]=$+{items}})\)))
(?(DEFINE)(?'dataarray'\<((?&item),)*+(?&item)\>))
(?(DEFINE)(?'item'(?P<itemx>(?:(?&dictionary)|(?&array)|(?&data)|(?&dataarray)|(?&datahex)|(?&string)|(?&number)|(?&boolean)))(?{$item[$itemndx++]=$+{itemx}})))
!;
sub printvalue {
my $depth = $_[0];
my $lvalue = $_[1];
if ( $lvalue =~ /^$regex(?&array)/ ) {
my $litems = $items[$itemsndx-1];
printf("[");
#print "«[" . $litems . "]»\n";
my $needcomma = 0;
while ( $litems =~ /$regex(?&item),?/g ) {
#print "«“" . $item[$itemndx-1] . "”»\n";
my $litem = $item[$itemndx-1];
if ($needcomma) {
printf (",");
}
printf ("\n%*s", $depth * 2 + 2, "");
printvalue($depth + 1, $litem);
$needcomma = 1;
}
if ($needcomma) {
printf ("\n%*s", $depth * 2, "");
}
printf("]");
}
elsif ( $lvalue =~ /^$regex(?&dictionary)/ ) {
my $lfields = $fields[$fieldsndx-1];
printf("{");
#print "«{" . $lfields . "}»\n";
my $needcomma = 0;
while ( $lfields =~ /$regex(?&field)(,?)/g ) {
#print "«“" . $field[1] . " = " . $item[$itemndx-1] . "”»\n";
my $lfield = $field[0];
my $litem = $item[$itemndx-1];
if ($needcomma) {
printf (",");
}
printf("\n%*s\"%s\" : ", $depth * 2 + 2, "", $lfield);
printvalue($depth + 1, $litem);
$needcomma = 1;
}
if ($needcomma) {
printf ("\n%*s", $depth * 2, "");
}
printf("}");
}
elsif ( $lvalue =~ /^$regex(?&datahex)/ ) {
$lvalue =~ s/$regex(?&indent)(?P<achar>.)/' ' x ($depth * 2 + 2) . $+{achar}/ge;
print $lvalue;
}
else {
print $lvalue;
}
}
sub printnode {
my $depth = $_[0];
printf ("%*s\"%s\" : {", $depth * 2, "", $nodename);
$depth += 1;
my $needcomma = 0;
for (my $ndx = 0; $ndx < $propertyndx; $ndx++) {
if ($needcomma) {
printf (",");
}
printf ("\n");
printf ("%*s\"%s\" : ", $depth * 2, "", $property[$ndx]);
printvalue($depth, $value[$ndx]);
$needcomma = 1;
}
if ($needcomma) {
printf "\n";
}
print "}";
}
sub printioreg {
my $needcomma = 0;
while ( $_ =~ /$regex(?&node)/g ) {
if ($needcomma) {
printf (",");
}
printf ("\n");
#printf ("%*s\"%s\" : {}", $depth * 2, "", $nodename);
printnode(0);
$needcomma = 1;
}
}
my %root = (
_1_indent => -1,
_9_children => []
);
my $prevnode = \%root;
my %dispnodes = ();
sub parsevalue {
my $depth = $_[0];
my $path = $_[1];
my $lvalue = $_[2];
if ( $lvalue =~ /^$regex(?&array)/ ) {
my $litems = $items[$itemsndx-1];
my @thearray = ();
my $arrayndx = 0;
while ( $litems =~ /$regex(?&item),?/g ) {
my $litem = $item[$itemndx-1];
$thearray[$arrayndx] = parsevalue($depth + 1, $path . '[' . $arrayndx . ']', $litem);
$arrayndx++;
}
return \@thearray;
}
elsif ( $lvalue =~ /^$regex(?&dictionary)/ ) {
my $lfields = $fields[$fieldsndx-1];
#print "«{" . $lfields . "}»\n";
my %thedict = ();
while ( $lfields =~ /$regex(?&field)(,?)/g ) {
my $lfield = $field[0];
my $newpath = $path . '/' . $lfield;
if ($parseall || ($newpath =~
m"
TimingElements\[\d+\](/
(
(
ColorModes(\[\d+\]/
ID
)?
)|
(
HorizontalAttributes(/
(
\w+
)
)?
)|
(
VerticalAttributes(/
(
\w+
)
)?
)|
ID|
IsInterlaced|
IsOverscanned|
IsPreferred|
IsPromoted|
IsSplit|
IsVirtual
)
)?
$"x))
{
my $litem = $item[$itemndx-1];
$thedict{$lfield} = parsevalue($depth + 1, $newpath, $litem);
}
}
return \%thedict;
}
elsif ( $lvalue =~ /^$regex(?&datahex)/ ) {
$lvalue =~ s/^[ |]+[0-9A-F]+:((?: [0-9A-F]{2}){1,32}) .*/$1/gm;
$lvalue =~ s/[\n ]//gm;
return "<" . $lvalue . ">";
}
elsif ( $lvalue =~ /^$regex(?&number)/ ) {
return $lvalue + 0;
}
elsif ( $lvalue =~ /^$regex(?&string)/ ) {
return $string[$stringndx-1];
}
elsif ( $lvalue =~ /^$regex(?&boolean)/ ) {
return ( $lvalue eq "Yes" ? JSON::PP::true : JSON::PP::false );
}
else {
return $lvalue;
}
}
sub parsenode {
$thepath =~ s|^((/[^/]*){$nodeindent}).*|$1/$nodename|;
my %thedict = (
_1_indent => $nodeindent,
_2_name => $nodename,
_3_path => $thepath,
_4_class => $classname,
_5_id => $nodeid,
_6_flags => $nodeflags,
_7_properties => {},
_9_children => []
);
my $parent = $prevnode;
while ($parent->{"_1_indent"} >= $nodeindent) {
$parent = $parent->{"_8_parent"};
}
$thedict{"_8_parent"} = $parent;
for (my $ndx = 0; $ndx < $propertyndx; $ndx++) {
if ( $parseall || ($property[$ndx] =~ /TimingElements|DPTimingModeId/) ) {
#print "value:" . $value[$ndx] . "\n";
$thedict{"_7_properties"}{$property[$ndx]} = parsevalue($nodeindent, $thepath . "/" . $property[$ndx], $value[$ndx]);
}
}
push(@{$parent->{"_9_children"}}, \%thedict);
$prevnode = \%thedict;
#print "node:\n";
#say Dumper \%thedict;
#print "\n";
return \%thedict;
}
sub parseioreg {
my $thedispnodename = "";
while ( /$regex(?&node)/g ) {
if ($nodename =~ /^(disp\d+)@.*/) {
$thedispnodename = "$1";
}
elsif ($nodename =~ /^(dispext\d+)@.*/) {
$thedispnodename = "$1";
}
my $thedict = parsenode();
if ( $nodename eq "AppleCLCD2" ) {
$dispnodes{$thedispnodename} = $thedict;
}
}
}
sub dumpstruct {
my $indenting = -1;
my $path = "";
my %donenodes = ();
my $inner; $inner = sub {
my $ref = $_[0];
my $key = $_[1];
$indenting++;
print ' ' x ($indenting * 4);
if ($key) {
print '"',$key,'"',' : ';
}
if (ref $ref eq 'ARRAY') {
if (exists $donenodes{$ref}) {
print "@" . $donenodes{$ref};
}
else {
$donenodes{$ref} = $path;
print "[";
my $needcomma = 0;
for my $k(@{$ref}) {
if ($needcomma) {
print ",";
}
print "\n";
$inner->($k);
$needcomma = 1;
}
#$inner->($_) for @{$ref};
print "\n",' ' x ($indenting * 4),"]";
}
}
elsif (ref $ref eq 'HASH'){
if (exists $donenodes{$ref}) {
print "%" . $donenodes{$ref};
}
else {
$donenodes{$ref} = $path;
print "{";
my $needcomma = 0;
for my $k(sort keys %{$ref}){
if ($needcomma) {
print ",";
}
print "\n";
$inner->($ref->{$k},$k);
$needcomma = 1;
}
print "\n",' ' x ($indenting * 4),"}";
}
}
elsif ( JSON::PP::is_bool($ref) ) {
print ($ref ? "true" : "false");
}
elsif (($ref ^ $ref) ne '0') {
print '"',$ref,'"';
}
else {
print $ref;
}
$indenting--;
};
$inner->($_) for @_;
print "\n";
}
sub dumpresolutions {
for my $j(sort keys %dispnodes){
print "\n$j:\n";
for my $k(@{$dispnodes{$j}{"_7_properties"}{"TimingElements"}}) {
my %l = %$k;
printf(
"%s%dx%d%s@%.3fHz %.3fkHz %.2fMHz h(%d %d %d %s) v(%d %d %d %s) %s%s%s%s%s\n",
((exists $dispnodes{$j}{"_7_properties"}{"DPTimingModeId"}) && $l{"ID"} eq $dispnodes{$j}{"_7_properties"}{"DPTimingModeId"}) ? " -> " : " ",
$l{"HorizontalAttributes"}{"Active"},
$l{"VerticalAttributes"}{"Active"},
$l{"IsInterlaced"} ? "i" : "",
$l{"VerticalAttributes"}{"PreciseSyncRate"} / 65536.0,
$l{"HorizontalAttributes"}{"PreciseSyncRate"} / 65536.0,
$l{"HorizontalAttributes"}{"PreciseSyncRate"} * $l{"HorizontalAttributes"}{"Total"} / 65536000.0,
$l{"HorizontalAttributes"}{"FrontPorch"},
$l{"HorizontalAttributes"}{"SyncWidth"},
$l{"HorizontalAttributes"}{"BackPorch"},
$l{"HorizontalAttributes"}{"SyncPolarity"} ? "+" : "-",
$l{"VerticalAttributes"}{"FrontPorch"},
$l{"VerticalAttributes"}{"SyncWidth"},
$l{"VerticalAttributes"}{"BackPorch"},
$l{"VerticalAttributes"}{"SyncPolarity"} ? "+" : "-",
$l{"IsOverscanned"} ? " (overscan)" : "",
$l{"IsPreferred"} ? " (preferred)" : "",
$l{"IsPromoted"} ? " (promoted)" : "",
$l{"IsSplit"} ? " (tiled)" : "",
$l{"IsVirtual"} ? " (virtual)" : ""
);
}
}
}
my $utf8_encoded_json_text = "";
while (<>) {
#printioreg();
parseioreg();
#dumpstruct($root{"_9_children"}); # outputs JSON with indents
#$utf8_encoded_json_text = encode_json( \%root ); # outputs JSON with no white space
#print $utf8_encoded_json_text . "\n";
dumpresolutions();
}
@TM22SFA
Copy link

TM22SFA commented Mar 15, 2021

Hello, your script shows me 4k@120Hz resolution:
3840x2160@119.910Hz 266.561kHz 1066.51MHz h(48 34 79 +) v(4 6 53 -) (preferred)
Is there a way to enabled it? "System Preferences"->Displays->Scaled only shows me 4k@60Hz.

@joevt
Copy link
Author

joevt commented Mar 16, 2021

@TM22SFA, I don't know of a method to add or enable timings on M1 Macs. Please report such issues to Apple and maybe they'll fix it one day.

@joevt
Copy link
Author

joevt commented Nov 18, 2021

Updated for M1 Max and M1 Pro. Tested with ioreg from M1 Max with four displays connected.
https://forums.macrumors.com/threads/how-many-monitors-do-m1-pro-max-support-without-hdmi.2322772/post-30625868

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