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;