scripts/sway/wallpaper.pl

335 lines
11 KiB
Perl
Raw Normal View History

2022-07-23 05:12:03 +00:00
#!/usr/bin/env perl
use strict;
use warnings;
use POSIX;
2022-07-23 05:12:03 +00:00
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";
2022-07-23 05:12:03 +00:00
use AnyEvent::Sway;
our $s = AnyEvent::Sway->new();
our $o = $s->get_outputs->recv();
die "No outputs detected.\n" unless (scalar(@$o));
2022-07-23 05:12:03 +00:00
sub usage()
{
print("$0 [output(s)] [-d|--daemon]\n
2022-07-23 05:12:03 +00:00
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> Path to wallpaper directory.
-p <path> 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
2022-07-23 05:12:03 +00:00
--help This menu
-h\n");
exit(0);
2022-07-23 05:12:03 +00:00
}
# SIGUSR reset the timer, force immediate reload, continue looping
2022-07-23 05:12:03 +00:00
$SIG{USR1} = sub {
alarm 0;
run(%settings);
2022-07-23 05:12:03 +00:00
};
# SIGTERM reset the timer, clean pid, and allow for the main loop to finish run
2022-07-23 05:12:03 +00:00
$SIG{TERM} = sub {
alarm 0;
clean();
$settings{daemon} = 0;
2022-07-23 05:12:03 +00:00
};
# SIGKILL clean pid then exit immediately
$SIG{KILL} = sub {
clean();
exit(0);
2022-07-23 05:12:03 +00:00
};
sub clean
{
if (-e $pidfile) {
open (my $fh, '<', $pidfile);
my $p = <$fh>;
close($fh);
chomp $p;
kill($p);
unlink($pidfile);
}
2022-07-23 05:12:03 +00:00
}
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;
2022-07-23 05:12:03 +00:00
}
2022-07-23 05:12:03 +00:00
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;
2022-07-23 05:12:03 +00:00
}
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 );
2022-07-23 05:12:03 +00:00
}
sub get_active
{
my @targets;
foreach (@$o) {
push (@targets, $_->{name});
}
return @targets;
2022-07-23 05:12:03 +00:00
}
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'";
2022-07-23 05:12:03 +00:00
}
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
################################################################################
2022-07-23 05:12:03 +00:00
my @targets;
my $daemon;
my $delay;
my $crop;
2022-07-23 05:12:03 +00:00
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);
}
2022-07-23 05:12:03 +00:00
}
################################################################################
# Validate arguments
################################################################################
2022-07-23 05:12:03 +00:00
die "Invalid rotation delay: $delay" if (defined($delay) && $delay !~ m/^\d+$/);
die "Invalid wallpaper path '$path'. Not a directory." if (defined($path) && !-d $path);
2022-07-23 05:12:03 +00:00
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;
2022-07-23 05:12:03 +00:00
}
$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
2022-07-23 05:12:03 +00:00
}
################################################################################
# Main
################################################################################
2022-07-23 05:12:03 +00:00
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
################################################################################
2022-07-23 05:12:03 +00:00
if (scalar(@e)) {
die "Failure while not running as daemon:\n" .
join("\n",@e);
2022-07-23 05:12:03 +00:00
}
exit(0);