#!/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"; # Temporary directory to save generated waybar config(s) my $waybar_temporary = '/tmp'; # File to log and recover last used layout name my $last = "$ENV{'HOME'}/.config/last_display"; ######################################################################## # 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', '3CQ4342S6W' => '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 }, 'LVDS' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 0, 'y' => 0, 'rotate' => 0, 'waybar' => 'bottom' } }, 'stacked' => { 'Sam' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 960, 'y' => 0, 'rotate' => 0, 'waybar' => 'bottom' }, 'HP-1' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 0, 'y' => 1200, 'rotate' => 0, 'waybar' => 'top' }, 'HP-2' => { 'on' => 1, 'width' => 1920, 'height' => 1080, 'x' => 1920, 'y' => 1200, 'rotate' => 0, 'waybar' => 'top' }, 'LVDS' => { '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, } } ); ######################################################################## # Program (do not edit below) ######################################################################## use strict; use warnings; my $waybar_only = 0; 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; } # Bail if requested config doesn't exist unless (defined $configs{$config}) { 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 -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}; } } # 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 = `sway output $_ disable`; my $res = $json->decode($res_raw)->[0]; # If failed, print to STDERR unless ($res->{success}) { print STDERR "Error ($res->{error}) in command 'sway 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; # SIGKILL match # TODO BUG: when multiple processes are running, some respawn with new PIDs. IDK why? } 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 foreach my $out (keys %$on) { unless ($waybar_only) { # Build command, starting by enabling and powering on my $cmd = "sway output $on->{$out}->{output} enable dpms on"; # If additional options are provided, add them to command 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}) { $waybar =~ s/__WIDTH__/$on->{$out}->{width}/; # If width is not set, comment that line out to use default } else { $waybar =~ s/([^\s]*\s*)__WIDTH__/\/\/ $1__WIDTH__/gg; } } } # 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 my $tmp = $waybar_temporary . "/waybar-" . time() .".config"; open ($fh, '>', $tmp); print $fh $waybar; close $fh; `nohup waybar --config=$tmp`; # Remove config unlink $tmp; }