#!/usr/bin/env perl use strict; use warnings; use POSIX; use lib "$ENV{HOME}/perl5/lib/perl5"; our $debug = 1; # For testing, will output configuration and errors if true our %settings; our @e; our $pidfile = "/tmp/$ENV{USER}-wallpaper.pid"; use AnyEvent::Sway; our $s = AnyEvent::Sway->new(); our $o = $s->get_outputs->recv(); die "No outputs detected.\n" unless (scalar(@$o)); 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); } # SIGUSR reset the timer, force immediate reload, continue looping $SIG{USR1} = sub { alarm 0; run(%settings); }; # SIGTERM reset the timer, clean pid, and allow for the main loop to finish run $SIG{TERM} = sub { alarm 0; clean(); $settings{daemon} = 0; }; # SIGKILL clean pid then exit immediately $SIG{KILL} = sub { clean(); exit(0); }; sub clean { if (-e $pidfile) { open (my $fh, '<', $pidfile); my $p = <$fh>; close($fh); chomp $p; kill($p); unlink($pidfile); } } sub choose_image { my @w = glob("$settings{path}/*"); return undef unless (scalar(@w)); my @i; foreach (@w) { if (-d "$settings{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'"; } sub run { my @err; # Local copy of targets so that it will re-check active every time my @t = $settings{targets} if (defined($settings{targets})); unless (scalar(@t)) { @t = get_active(); push(@err, "No target outputs") unless (scalar(@t)); } foreach my $a (@t) { my $image = choose_image($settings{path}); if (defined($image)) { if ( -r "$image" ) { print "selected $image\n"; my $cropped; if ($settings{crop}) { if ($settings{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 ($settings{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 $settings{path}"); } } if ($settings{debug} && $settings{daemon}) { print STDERR join("\n",@err); } else { @e = @err; } } ################################################################################ # Collect arguments ################################################################################ my @targets; my $daemon; my $delay; my $crop; 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); } } ################################################################################ # Validate arguments ################################################################################ die "Invalid rotation delay: $delay" if (defined($delay) && $delay !~ m/^\d+$/); die "Invalid wallpaper path '$path'. Not a directory." if (defined($path) && !-d $path); 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; } $settings{targets} = @targets || undef; $settings{daemon} = $daemon || 0; $settings{path} = $path || "$ENV{HOME}/wallpapers"; $settings{crop} = $crop || 1; $settings{delay} = $delay || 300; $settings{debug} = $debug || 0; ################################################################################ # For if daemonized ################################################################################ if ($settings{daemon}) { my $p = fork(); if ($p) { if (open(my $fh, ">", $pidfile)) { print $fh "$p" || die "Failed to write pid ($p) to pidfile $pidfile"; close($fh); } else { print "Failed to open pidfile: $pidfile: $!\n"; } exit(0); } #setpgrp; #setsid; umask 0; # TODO: configure systemD logging } ################################################################################ # Main ################################################################################ do { run(%settings); if ($settings{daemon}) { my $normal = "reload wallpaper"; eval { $SIG{ALRM} = sub { print "$normal\n" }; alarm $settings{delay}; POSIX::pause(); alarm 0; }; if ($@ && $@ !~ quotemeta($normal)) { if ($settings{debug}) { print $normal; } } } } while ($settings{daemon}); ################################################################################ # If we made it to here, it was not daemonized. Output errors and exit ################################################################################ if (scalar(@e)) { die "Failure while not running as daemon:\n" . join("\n",@e); } exit(0);