From 4716ce55bce0b7178e3a88b3af41cba3bc5a3efb Mon Sep 17 00:00:00 2001 From: John Mertz Date: Sat, 23 Jul 2022 01:12:03 -0400 Subject: [PATCH] Wallpaper rotation script --- sway/wallpaper.pl | 292 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100755 sway/wallpaper.pl diff --git a/sway/wallpaper.pl b/sway/wallpaper.pl new file mode 100755 index 0000000..0cc9f6b --- /dev/null +++ b/sway/wallpaper.pl @@ -0,0 +1,292 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use lib "$ENV{HOME}/perl5/lib/perl5"; + +our $debug = 1; # For testing, will output configuration and errors if true + +use AnyEvent::Sway; +our $s = AnyEvent::Sway->new(); + +our $o = $s->get_outputs->recv(); +die "No outputs detected.\n" unless (scalar(@$o) > 1); + +sub usage() +{ + print("$0 [output(s)] [-d|--daemon]\n +Sets a wallpaper by cropping an appropriate sized section of a larger image.\n +All arguments other than the below are assumed to be output names, as defined +in my sway/displays.pl script. If no outputs are provided, all available that +are currently enabled will be set.\n +--path= Path to wallpaper directory. +-p Default: $ENV{HOME}/wallpapers\n +--daemon=N Configures the wallpaper to be periodically rotated for all +-d N of the given outputs. N indicates the number of seconds between + each rotation, if provided (default: 300).\n +--help This menu +-h\n"); + exit(0); +} + +$SIG{USR1} = sub { + # TODO: Update the timeout method so that the daemon will simply run the job as a function. + # when SIGUSR1 is received, force the alarm to expire immediately in order to rotate the image without delay. + print "This should rotate the image, but it doesn't yet"; +}; + +$SIG{TERM} = sub { + clean(); + exit(0); +}; + +$SIG{INT} = sub { + clean(); + exit(0); +}; + +sub clean +{ + my $pidfile = "/var/run/$ENV{HOME}-wallpaper.pid"; + if (-e $pidfile) { + open (my $fh, '<', $pidfile); + my $p = <$fh>; + chomp $p; + kill($p); + unlink($pidfile); + } +} + +sub choose_image +{ + my $path = shift; + + my @w = glob("$path/*"); + return undef unless (scalar(@w)); + + my @i; + foreach (@w) { + if (-d "$path/$_") { + next; + } + if ($_ =~ m/\.(png|jpg)$/) { + push(@i,$_); + } + } + + return $i[rand(scalar(@i))] || return undef; +} + +sub get_size +{ + my $target = shift; + foreach my $output (@$o) { + if ($output->{'name'} eq $target) { + return ( + #$output->{'current_mode'}->{'width'}, + #$output->{'current_mode'}->{'height'} + $output->{'rect'}->{'width'}, + $output->{'rect'}->{'height'} + ); + } + } + return undef; +} + +sub crop +{ + my $image = shift; + my $ow = shift; + my $oh = shift; + + my $cropped = $image; + $cropped =~ s#^.*/([^/]*)\.([^\.]+)$#$1-cropped.$2#; + + use Image::Magick; + my $im = Image::Magick->new(); + + $im->Read($image); + my ($iw, $ih) = $im->Get("columns", "rows"); + + # Return full size if it is smaller in either dimension then the output + if ($iw < $ow || $ih < $oh) { + print "Not cropping $image because it is too small\n"; + $cropped =~ s#-cropped\.([^\.]+)$#\.$1#; + use File::Copy; + File::Copy::copy($image,$cropped); + return $cropped if (-e $cropped); + return undef; + } + + print "Image size: $iw $ih\n"; + print "output size: $ow $oh\n"; + + my ($x, $y); + $x = int(rand($iw-$ow)); + $y = int(rand($ih-$oh)); + + print "Cropping $image ${ow}x${oh}+${x}+${y}\n"; + my $err = $im->Crop(geometry=>"${ow}x${oh}+${x}+${y}"); + die "$err" if ($err); + print "Writing $cropped\n"; + $err = $im->Write($cropped); + die "$err" if ($err); + return $cropped if ( -e $cropped ); +} + +sub get_active +{ + my @targets; + foreach (@$o) { + push (@targets, $_->{name}); + } + return @targets; +} + +sub set_background +{ + my $target = shift || return "No target or image provided"; + my $cropped = shift || return "No image provided"; + # TODO get fallback from javascript + my $cmd = "output $target background $cropped fill #000000"; + print "Running $cmd\n"; + my $ret = $s->message(0,$cmd)->recv; + if ($ret->[0]->{success}) { + print "Success!\n"; + return undef; + } + return "Failed to run Sway IPC command '$cmd'"; +} + +my @targets; +my $daemon = 0; +my $delay = 300; +my $crop = 1; +my $path; + +while (my $arg = shift(@ARGV)) { + if ($arg eq '-h' || $arg eq '--help') { + usage(); + } elsif ($arg =~ m/^\-\-path=?(.+)$/) { + die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); + $path = $1; + } elsif ($arg eq '-p') { + die "Redundant argument '$arg'. Wallpaper path already set.\n" if ($path); + $path = shift(@ARGV); + } elsif ($arg =~ m/^\-\-daemon=?(.+)$/) { + die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); + $delay = $1; + $daemon = 1; + } elsif ($arg eq '-d') { + die "Redundant argument '$arg'. Daemon mode already set.\n" if ($daemon); + if ($ARGV[0] =~ m/^\d+$/) { + $delay = shift(@ARGV); + } + $daemon = 1; + } elsif ($arg eq '--nocrop' || $arg eq '-') { + die "Redundant argument '$arg'. No-crop already set.\n" unless ($crop); + } else { + push(@targets,$arg); + } +} + +die "Invalid rotation delay: $delay" unless ($delay =~ m/^\d+$/); + +if ($path) { + die "Invalid wallpaper path '$path'. Not a directory." unless (-d $path); +} else { + $path = "$ENV{HOME}/wallpapers"; +} + +if (scalar(@targets)) { + my @kept; + foreach my $t (@targets) { + my $hit = 0; + foreach (@$o) { + if ($_->{name} eq $t) { + push(@kept, $t); + $hit++; + last; + } + } + print STDERR "Requested output '$t' not found\n" unless ($hit); + } + die "None of the requested outputs are active" unless (scalar(@kept)); + @targets = @kept; +} + +if ($daemon) { + my $p = fork(); + if ($p) { + my $pidfile = "/var/run/$ENV{HOME}-wallpaper.pid"; + open(my $fh, ">", $pidfile) || die "Failed to open pidfile: $pidfile"; + print $fh "Daemonized as PID: $p\n" || die "Failed to write pid ($p) to pidfile $pidfile"; + close($fh); + exit(0); + } +} + +if ($debug) { + print "Initial configuration:\n"; + print " Targets: ( " . ( join(" ",@targets) || "All active" ) . " )\n"; + print " Daemon: $daemon\n"; + print " Path: $path\n"; +} + +my @e; +do { + my @err; + # Local copy of targets so that it will re-check active every time + my @t = @targets; + unless (scalar(@t)) { + @t = get_active(); + push(@err, "No target outputs") unless (scalar(@t)); + } + foreach my $a (@t) { + my $image = choose_image($path); + if (defined($image)) { + if ( -r "$image" ) { + print "selected $image\n"; + my $cropped; + if ($crop) { + if ($debug) { + print "Cropping image for '$a' using '$image'\n"; + } + my ($ow, $oh) = get_size($a); + $cropped = crop($image, $ow, $oh); + unless ($cropped) { + push(@err, "Failed to generate cropped image") unless ($cropped); + next; + } + } else { + $cropped = $image; + } + my $e = set_background($a,$cropped); + push(@err, $e) if (defined($e)); + if ($crop) { + print "Deleting $cropped\n"; + #unlink($cropped) || push(@err, "Failed to delete $cropped after setting: $!"); + } + } else { + push(@err, "$a: Unable to read image $image"); + } + } else { + push(@err, "$a: Unable to select image from $path"); + } + } + if ($debug && $daemon) { + print STDERR join("\n",@err); + sleep($delay); + } else { + @e = @err; + } +} while ($daemon); + +if (scalar(@e)) { + die "Failure while not running as daemon:\n" . + join("\n",@e); +} + +exit(0); +