package Local::Desktop;
# graphical desktop management functions
#
# Copyright (C) 2020-2022 Sean Whitton
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
use 5.028;
use strict;
use warnings;
use Carp;
use JSON;
use File::Find;
use File::LibMagic;
use File::Spec::Functions "rel2abs";
use Exporter "import";
use File::Copy;
use List::Util "first";
our @EXPORT = qw(
$wmipc wmipc
fresh_workspace
compact_workspaces
select_wallpaper_files
ensure_resize_for_current_outputs
resize_for_current_outputs
pick_random_wallpapers );
`sh -c "command -v i3-msg"`;
our $wmipc = $? == 0 ? "i3-msg" : "swaymsg";
sub wmipc { system "$wmipc -q " . join ", ", @_ }
my $output_re = qr/ ([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+) /;
my @all_workspaces = (
"1", "2", "3", "4", "5", "6",
"7", "8", "9", "10", "11:F1", "12:F2",
"13:F3", "14:F4", "15:F5", "16:F6", "17:F7", "18:F8",
"19:F9", "20:F10", "21:F11", "22:F12"
);
=head fresh_workspace($send)
Switch to the next free workspace, if any. Return the name of that workspace,
or undef if no workspace was available. If $send, send the current window to
the fresh workspace instead of switching focus there.
=cut
sub fresh_workspace {
my $next_free_workspace = compact_workspaces(leave_gap => 1);
if ($next_free_workspace) {
my @cmds;
my %opts = @_;
push @cmds, "move container to workspace $next_free_workspace"
if $opts{send};
# When !$opts{go} we should execute neither of these commands, but we
# must work around .
#
# In the case that !$opts{go}, can use 'C-i S-;' to move any other
# wanted containers over, before finally going there with 'C-i ;'.
push @cmds, "workspace $next_free_workspace";
push @cmds, "workspace back_and_forth" unless $opts{go};
wmipc @cmds;
}
$next_free_workspace
}
=head compact_workspaces(%opts)
Rename workspaces so as to remove gaps in the sequence of workspaces.
If C<$opts{leave_gap}>, ensure there is a gap of one workspace after the
currently focused workspace and return the name of the gap workspace, or just
return undef if there is no space for a gap.
=cut
sub compact_workspaces {
my %opts = @_;
my @workspaces = @{ decode_json `$wmipc -t get_workspaces` };
@workspaces < @all_workspaces or return;
my ($current_workspace, $gap_workspace);
if ($opts{leave_gap}) {
$_->{focused} and $current_workspace = $_->{name}, last
for @workspaces
}
my ($i, @cmds);
while (my $next = shift @workspaces) {
my $workspace = $all_workspaces[$i++];
$opts{leave_gap}
and $next->{name} eq $current_workspace
and $gap_workspace = $all_workspaces[$i++];
next if $next->{name} eq $workspace;
my $pair = [$next->{name}, $workspace];
_wsnum($next->{name}) > _wsnum($workspace)
? push @cmds, $pair
: unshift @cmds, $pair
}
wmipc map "rename workspace $_->[0] to $_->[1]", @cmds;
$opts{leave_gap} and $gap_workspace
}
=head select_wallpaper_files(@files)
Select the first entry of @files as the wallpaper for the first output, the
second entry of @files as the wallpaper for the second output, etc.
This function works by creating copies of those wallpapers in ~/local.
=cut
sub select_wallpaper_files {
my $i;
unlink <"$ENV{HOME}/local/wallpaper??.*">;
for (@_) {
-r or croak "$_ could not be read!";
copy $_,
sprintf "$ENV{HOME}/local/wallpaper%02d." . (/\.([^.]+)\z/)[0], $i++;
}
}
=head resize_for_current_outputs()
Based on the output of xrandr(1), create ~/local/wallpaper.png from
~/local/wallpaperNN.*, such that executing `feh --bg-scale --no-xinerama
~/local/wallpaper.png` will put wallpaper00.* on the first output,
wallpaper01.* on the second, etc. There should not be more than one file
matching ~/local/wallpaperNN.* for each NN.
Returns the value of $? right after executing convert(1).
=cut
sub resize_for_current_outputs {
# note that swaybg and swaylock have per-output wallpapers built in, so
# this function is only for X11
chomp(my @xrandr = `xrandr`);
my ($canvas_w, $canvas_h) = _get_screen_size(@xrandr);
# sort the displays from left to right and then from top to bottom
my @displays = sort {
my (undef, undef, $a_x, $a_y) = $a =~ $output_re;
my (undef, undef, $b_x, $b_y) = $b =~ $output_re;
$a_y <=> $b_y or $a_x <=> $b_x
} grep / connected /, @xrandr;
my @wallpapers = sort {
($a =~ /wallpaper([0-9]+)\./)[0] <=> ($b =~ /wallpaper([0-9]+)\./)[0]
} <"$ENV{HOME}/local/wallpaper??.*">;
my @args = ("-page", "${canvas_w}x${canvas_h}", qw(-background none));
for (@displays) {
/$output_re/;
push @args, '(', (shift @wallpapers || "canvas:#FFFFF6"),
"-resize", "${1}x${2}^",
qw(-gravity center -extent), "${1}x${2}",
"-repage", "${1}x${2}+${3}+${4}", ')';
}
push @args, qw(-layers merge), "$ENV{HOME}/local/wallpaper.png";
system "convert", @args;
return $?;
}
=head ensure_resize_for_current_outputs()
Call C if it looks to be needed.
=cut
sub ensure_resize_for_current_outputs {
return unless <"$ENV{HOME}/local/wallpaper??.*">;
resize_for_current_outputs(), return
unless -e "$ENV{HOME}/local/wallpaper.png";
my ($screen_w, $screen_h) = _get_screen_size(`xrandr`);
my $magic = File::LibMagic->new;
my ($img_w, $img_h)
= $magic->info_from_filename("$ENV{HOME}/local/wallpaper.png")
->{description} =~ / ([0-9]+) x ([0-9]+),/;
resize_for_current_outputs()
if $img_w
and $img_h
and ($screen_w != $img_w or $screen_h != $img_h);
}
=head pick_random_wallpapers($n, @dirs)
Pick C<$n> random wallpapers from files in any of the directories listed in
C<@dirs>.
=cut
sub pick_random_wallpapers {
my $n = shift;
my $magic = File::LibMagic->new(follow_symlinks => 1);
my (@images, @picks);
find sub {
push @images, $File::Find::name
if $magic->info_from_filename($_)->{description} =~ /^\w+ image/
}, @_;
push @picks, splice @images, int(rand @images), 1 for 1 .. $n;
return @picks;
}
sub _get_screen_size {
chomp(@_);
my ($canvas) = grep /^Screen 0:/, @_;
$canvas =~ /current ([0-9]+) x ([0-9]+)/;
}
sub _wsnum { (split /:/, $_[0])[0] }
1;