#!/usr/bin/perl ######################################################################## # Usage ######################################################################## # # $ displays.pl [layout] [-w] # # layout Name of desired layout, as configured starting at line 100 # If missing, last known layout is used (logged to file $last) # -w (Re)load waybar only. Skip display configuration. Only kill # existing waybars and start new ones for desired layout ######################################################################## # Dependencies ######################################################################## # # Depends on JSON::XS and Proc::ProcessTable # # Debian: # apt install libjson-xs-perl libproc-processtable-perl ######################################################################## # Directories and Files ######################################################################## # Template is a normal config with the following in place of values: # "output": __OUTPUT__ # "position": __POSITION__ # "width": __WIDTH__ (optional) my $waybar_template = "$ENV{'HOME'}/.config/waybar/config.template"; #my $swaysock = "/home/jpm/.config/sway-ipc.sock"; my $swaysock = `ls /run/user/1000/sway-ipc.1000*`; chomp($swaysock); # Path to actual config file generated from template my $waybar_config = "$ENV{'HOME'}/.config/waybar/config"; # File to log and recover last used layout name my $last = "$ENV{'HOME'}/.config/last_display"; # File to log active outputs (used by swayidle, and to attempt to restore my $active_outputs = "$ENV{'HOME'}/.config/active_outputs"; ######################################################################## # Display Serials and Names ######################################################################## # Hash to match display Serial with a friendly name. # You can get the serial from (quoted with *): # $ swaymsg -t get_outputs # Output eDP-1 'Unknown 0x057D *0x00000000*' my %outputs = ( #'0x00000101' => 'Sam', 'HTNCB00059' => 'Sam', #'3CQ4342S6W' => 'HP-1', '0x00000101' => 'HP-1', '3CQ3310Q1Q' => 'HP-2', '0x00000000' => 'LVDS' ); ######################################################################## # Display Configurations ######################################################################## # First-level key of hash is the name of each configuration # Second-level keys are the display friendly-names, above # Third-level are the actual settings for that display my %configs = ( 'detached' => { 'HP-1' => { 'on' => 0 }, 'HP-2' => { 'on' => 0 }, 'Sam' => { 'on' => 0 }, 'eDP-1' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 0, 'y' => 0, 'rotate' => 0, 'waybar' => 'bottom', } }, 'stacked-laptop' => { 'Sam' => { 'on' => 1, 'width' => 1920, 'height' => 1200, 'x' => 960, 'y' => 0, 'rotate' => 0, 'waybar' => 'bottom' }, 'HP-1' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 1920, 'y' => 1200, 'rotate' => 0, 'waybar' => 'top' }, 'HP-2' => { 'on' => 0 }, 'eDP-1' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 0, 'y' => 1200, 'rotate' => 0, 'waybar' => 'bottom' } }, 'stacked' => { 'Sam' => { 'on' => 1, 'width' => 1920, 'height' => 1200, 'x' => 960, 'y' => 0, 'rotate' => 0, 'waybar' => 'bottom' }, 'HP-1' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 1920, 'y' => 1200, 'rotate' => 0, 'waybar' => 'top' }, 'HP-2' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 0, 'y' => 1200, 'rotate' => 0, 'waybar' => 'top' }, 'eDP-1' => { 'on' => 0 } }, 'sidebyside' => { 'HP-1' => { 'on' => 1, 'width' => 1080, 'height' => 1920, 'x' => 0, 'y' => 225, 'rotate' => 270, 'waybar' => 'top' }, 'Sam' => { 'on' => 1, 'width' => 1200, 'height' => 1920, 'x' => 1080, 'y' => 0, 'rotate' => 90, 'waybar' => 'top' }, 'HP-2' => { 'on' => 1, 'width' => 1080, 'height' => 1920, 'x' => 2280, 'y' => 225, 'rotate' => 90, 'waybar' => 'top' }, 'LVDS' => { 'on' => 0, } } ); my $hostname = `hostname`; chomp($hostname); if ($hostname eq "yoga.lan.john.me.tz") { $configs{'detached'}->{'eDP-1'}->{'width'} = 2560; $configs{'detached'}->{'eDP-1'}->{'height'} = 1440; $configs{'detached'}->{'eDP-1'}->{'scale'} = 1.333333; } ######################################################################## # Disable display from other laptop ######################################################################## if ($ENV{'SSH_AUTH_SOCK'} eq '/home/jpm/.ssh/ssh-agent.yoga.lan.john.me.tz.sock') { foreach my $layout (keys %configs) { delete($configs{$layout}{'LVDS'}); } $outputs{'0x00000000'} = 'eDP-1'; } else { foreach my $layout (keys %configs) { delete($configs{$layout}{'eDP-1'}); } } ######################################################################## # Program (do not edit below) ######################################################################## use strict; use warnings; my $waybar_only = 0; my $restore = 0; # Set if no config is provided. Requires validation. my $config; if (scalar(@ARGV)) { while (@ARGV) { my $arg = shift; if ($arg eq "-w") { $waybar_only = 1; } else { if (defined $config) { die "Too many arguments.\n"; } $config = $arg; } } } # Get previous config if one is not provided unless (defined $config) { open(my $fh, '<', $last) || die "Config name not provided and failed to open $last\n"; $config = <$fh>; close($fh); chomp $config; $restore = 1; } # Bail if requested config doesn't exist if (!defined($configs{$config})) { if ($restore) { # If restoration is attempted with invalid config, just enable eDP-1 $config = 'detached'; } else { die "$config is not a defined configuration: " . join(', ', keys %configs) . "\n"; } } # Write config that is to be used so that it can be recovered as default open(my $fh, '>', $last) || print STDERR "Config name cannot be written to $last\n"; print $fh $config; close($fh); # Fetch connected displays use JSON::XS; my $json = JSON::XS->new(); my $displays_raw = `swaymsg -s $swaysock -t get_outputs --raw`; my $displays = $json->decode($displays_raw); # For each connected display, collect the desired settings for enabled # displays and a list of displays to disable my $on; my @off; for (my $i = 0; $i < scalar(@$displays); $i++) { # If a display is found without any settings print error and turn it off unless (defined $configs{$config}{$outputs{$displays->[$i]->{serial}}}){ print STDERR "Output $displays->[$i]->{name} (" . $displays->[$i]->{serial} . ") found without defined function. Disabling.\n"; push @off, $displays->[$i]->{name}; next; } # If display is enabled, copy all of the desired settings if ($configs{$config}{$outputs{$displays->[$i]->{serial}}}{on}) { $on->{$outputs{$displays->[$i]->{serial}}} = $configs{$config}{$outputs{$displays->[$i]->{serial}}}; $on->{$outputs{$displays->[$i]->{serial}}}{output} = $displays->[$i]->{name}; # Otherwise simply list it for disabling } else { push @off, $displays->[$i]->{name}; } } # Verify that all requested displays are actually available my @unavailable; foreach my $output (keys %$on) { my $found = 0; foreach my $o (keys %outputs) { if ($outputs{$o} eq $output) { $found = 1; last; } } unless ($found) { push @unavailable, $output; } } if (scalar(@unavailable)) { die "Config requires unavailable output(s) " . join(', ', @unavailable) . "\n"; } # Skip enabling/disabling displays if only running waybar (re)start unless ($waybar_only) { # Number of simultaneous outputs is limited by gpu, so disabled displays # first foreach (@off) { # Sway returns status as JSON my $res_raw = `swaymsg -s $swaysock output $_ disable`; my $res = $json->decode($res_raw)->[0]; # If failed, print to STDERR unless ($res->{success}) { print STDERR "Error ($res->{error}) in command 'swaymsg" . " -s $swaysock output $_ disable'\n"; } } } # Kill existing Waybars require Proc::ProcessTable; my $t = new Proc::ProcessTable; foreach my $p ( @{ $t->table } ) { my $cmndline = $p->{'cmndline'}; $cmndline =~ s/\s*$//g; if ($cmndline =~ /^waybar.*/) { # Never kill this process if ($p->{'pid'} == $$) { next; } else { $p->kill(9); } } } # Load in config template my $template; if (open (my $fh, '<', $waybar_template)) { while (<$fh>) { $template .= $_; } close $fh; chomp $template; } else { print STDERR "Failed to load template $waybar_template\n"; } # If template is already set up as an array, remove the square brackets so that # we can concatenate multiple displays $template =~ s/^[^\[\{]*\[(.*)\]$/$1/s; # Setup waybar config file my $waybar = ''; # Configure each enabled display my @active; foreach my $out (keys %$on) { push @active, $on->{$out}->{output}; unless ($waybar_only) { # Build command, starting by enabling and powering on my $cmd = "swaymsg -s $swaysock output $on->{$out}->{output}" . " enable dpms on"; # If additional options are provided, add them to command if (defined $on->{$out}->{scale}) { $cmd .= " scale $on->{$out}->{scale}"; } if (defined $on->{$out}->{rotate}) { $cmd .= " transform $on->{$out}->{rotate}"; } if (defined $on->{$out}->{x} && defined $on->{$out}->{y}) { $cmd .= " position $on->{$out}->{x} $on->{$out}->{y}"; } if (defined $on->{$out}->{width} && defined $on->{$out}->{height} ) { $cmd .= " mode $on->{$out}->{width}x" . "$on->{$out}->{height}"; } # Sway returns status as JSON my $res_raw = `$cmd`; my $res = $json->decode($res_raw)->[0]; # If failed, print to STDERR unless ($res->{success}) { print STDERR "Error ($res->{error}) in command " . "'$cmd'\n"; } } # Skip waybar setup if template failed to be loaded if ( (defined $template) && (defined $on->{$out}->{waybar}) && ($on->{$out}->{waybar} =~ m/(top|bottom|left|right)/) ) { # If there's already a display set up, add a comma unless ($waybar eq '') { $waybar .= ','; } $waybar .= $template; # Replace basic preferences $waybar =~ s/__OUTPUT__/"$on->{$out}->{output}"/; $waybar =~ s/__POSITION__/"$on->{$out}->{waybar}"/; if (defined $on->{$out}->{width}) { my $x = $on->{$out}->{width}; if (defined $on->{$out}->{scale}) { $x = sprintf("%.0d", $x / $on->{$out}->{scale}); print "width: $x\n"; } $waybar =~ s/__WIDTH__/$x/; # If width is not set, comment that line out to use default } else { $waybar =~ s/([^\s]*\s*)__WIDTH__/\/\/ $1__WIDTH__/gg; } } } open($fh, '>', $active_outputs); print $fh join(' ', @active); close($fh); # Restore array formatting $waybar = '[' . $waybar . ']'; # Start Waybar as fork my $pid = fork; unless ($pid) { open STDIN, '/dev/null'; open STDOUT, '>>/dev/null'; open STDERR, '>>/dev/null'; # Write config to a temporary file open ($fh, '>', $waybar_config); print $fh $waybar; close $fh; my $waydisplay = $ENV{'WAYLAND_DISPLAY'} || 'wayland-0'; `WAYLAND_DISPLAY=$waydisplay nohup waybar --config=$waybar_config >> waybar.log`; }