rarsamx / Scripts

Sets the background in a multimonitor set up under Cinammon with either one image spanned across or one image per monitor
GNU General Public License v3.0
7 stars 2 forks source link

I've made some improvements to wmSetMultimonitorBkgrnd.sh #1

Open AlfredoLlaquet opened 2 years ago

AlfredoLlaquet commented 2 years ago

Hi. I use Cinnamon on Garuda Linux (Arch Linux, basically). I have 3 monitors and a directory with many many images. I don't know almost anything about bash scripting, but with DuckDuckGo and several hours of time I've made the following improvements to suit my needs:

-My version of the script works with directories with tens of thousands of images, like mine. The original script reported an error when this was the case. -The file names of the images now may contain spaces. The original script failed in that situation. -The original script didn't ever work, at least on my computer, because, when it accessed the images at some point, it did not include their path. -The script now, instead of zooming the images to occupy the whole screen, scales the images and fills the remaining space with black. -The original script did not access correctly the array of images and the same image was showing in two of the monitors. Now, a different image shows in each monitor. -In my version of the script, the interval is in seconds. -The original script listed the images every cycle. Now, it creates a temporal file with the listing at the beginning and accesses this file in subsequent cycles (less disk intensive). -My script works with modern image file formats such as webp. -My script has the limitation that it assumes that all files in the images directory are images. That is my case.

My version follows. It works great in my computer and it should also work in others that respect the limitation just mentioned. Feel free to do whatever you want with it:

#!/bin/bash

# This script creates an image to span multiple monitors under Cinnamon
# It receives only one parameter which can be
# - A single file name : The script resizes and shaves the image to fit the screeens
# - A directory with potential files: the script selects randomly one file per monitor.
#
# Note: Files are scaled and shaved to fit the display area without loosing aspect radio.
#
#Requires:
#  ImageMagick
#  xrandr
#
# Author: Raul Suarez
# https://www.usingfoss.com/
#
# License: GPL v3.0

#SCRIPTNAME=$(basename $_)
SCRIPTNAME=${0##*/}
#====== VALIDATE DEPENDENCIES ===

command -v gsettings >/dev/null 2>&1 || 
    { echo >&2 "This script only works on systems which rely on gsettings.  Aborting."; exit 1; }
command -v xrandr >/dev/null 2>&1 || 
    { echo >&2 "This script only works on systems which rely on xrandr.  Aborting."; exit 1; }
command -v identify >/dev/null 2>&1 || 
    { echo >&2 "Please install 'imagemagick'.  Aborting."; exit 1; }

#====== GLOBAL VARIABLES ===
VERSION=0.2.2
VALID=true
FORMATS="jpg|jpeg|png"
OUTIMG=${HOME}/.cinnamon/backgrounds/multiMonitorBackground.jpg

MONITORS=()
SCREENGEOMETRY=""
DIRECTORY=""
SINGLEIMG=false
VERBOSE=false
LOOP=false
INTERVAL=""
MYLIST=""
TMPFILE="/tmp/alf_images.tmp"

declare -a FILES=()

# Allow extended pattern substitutions
shopt -s extglob

#====== FUNCTIONS ===

showHelp () {
    echo "Usage: multiMonitorBackground [OPTIONS] [FILE] [FILE] ...

This script to set up a background image spaning multiple monitors 
under Cinnamon. If no parameters are specified: A random file per monitor is 
selected from current directory.

Note: Files are scaled and shaved to fit the display area without loosing aspect radio.

It can receive the following parameters:
 FILE          Files to set as background. FILE paths must be relative to the
               DIRECTORY unless they have an absolute path. Each FILE must be 
               an image file.
               - If the script receives less files than the number of monitors
                 it will cycle through the files repeating them until all
                 monitors have a file
               - If the script receives no files and the parameter -s is not 
                 specified the script will select one random image per active 
                 monitor
 -d DIRECTORY  A directory containing image files. The
               default is the current directory
 -s            Span a single FILE across monitors.  
               If no FILE is passed, and the parameter -s is present, a random
               file from the DIRECTORY is displayed.
 -t SECONDS    Time in SECONDS to refresh the background
 -version      Displays the version number
 -verbose      Displays additional information

Examples
: put the same image in each monitor
  ${SCRIPTNAME} mypic.jpg     
: span an image across all the monitors  
  ${SCRIPTNAME} mypic.jpg     
: select one random image per monitor from the directory indicated
  ${SCRIPTNAME} -d ~/Pics
: assign each image received to one or more monitors
  ${SCRIPTNAME} -d ~/Pics pic1.jpg pic2.jpg 

Requires:
  ImageMagick
  xrandr

Author: Raul Suarez
https://www.usingfoss.com/
"
}

readParameters () {
    DIRECTORY=""
    SINGLEIMG=false
    VERBOSE=false
    LOOP=false
    INTERVAL=""
    FILES=()

    while [ $# -gt 0 ] ; do
        unset OPTIND
        unset OPTARG
        while getopts :hsv:d:t:  options; do
            case $options in
                h)  showHelp
                    exit 0
                    ;;
                v)  local SECONDPART="$OPTARG"
                    [ "${SECONDPART}" != "ersion" ] && [ "${SECONDPART}" != "erbose" ] && 
                        echo "Invalid parameter -v$SECONDPART." && 
                        VALID=false
                    [ ${SECONDPART} == "ersion" ] && 
                        echo Version ${VERSION} && 
                        exit 0
                    [ ${SECONDPART} == "erbose" ] && 
                        VERBOSE=true
                    ;;
                d)  DIRECTORY="$OPTARG"
                    ;;
                s)  SINGLEIMG=true
                    ;;
                t)  INTERVAL=$((OPTARG))
                    LOOP=true
                    ;;
                :)  case $OPTARG in
                        v)  echo "Invalid parameter -v." >&2
                            ;;
                        t)  echo "Parameter -t usage -t <MINUTES>." >&2
                            ;;
                        d)  echo "Parameter -d usage -d <DIRECTORY>." >&2
                            ;;
                    esac
                    VALID=false
                    ;;
                ?)  echo "Invalid parameter -$OPTARG." >&2
                    VALID=false
                    ;;
            esac
        done
        shift $((OPTIND-1))
        FILES+=(${1})
        shift
    done

    [ -n "${DIRECTORY}" ] && [ -n "${FILES}" ] && assembleFullFileNames

    ${VERBOSE} && echo "Directory : ${DIRECTORY}"
    ${VERBOSE} && [ -n "${FILES}" ] && echo "Files : ${FILES[@]}"
    ${VERBOSE} && echo "Single image : ${SINGLEIMG}"
    ${VERBOSE} && ${LOOP} && echo "Refresh every : $((INTERVAL)) seconds"

    $VALID && validateParameters
    [ -z "${DIRECTORY}" ] && DIRECTORY="."
}

assembleFullFileNames () {

    declare -i i=0
    for FILE in "${FILES[@]}"
    do
        if [ "$(readlink -f ${FILE})" != "${FILE}" ]; then
            FILES[$i]="${DIRECTORY}/${FILES[$i]}"
        fi
        i+=1
    done
}

createTempFile () {
    if [ -z "${MYLIST}" ] || [ ! -f "${TMPFILE}" ] ; then
        ${VERBOSE} && echo "Creating temp list of files"
        MYLIST="done"
        ls "${DIRECTORY}" 2>/dev/null 1> "${TMPFILE}"
    fi
}

countImagesInDir () {
#    local TESTDIR="${1}"
#    echo $(ls "${TESTDIR}"/*.jpg "${TESTDIR}"/*.jpeg "${TESTDIR}"/*.png 2>/dev/null | wc -l)
# #     echo $(ls "${TESTDIR}"/*.@(${FORMATS}) 2>/dev/null | wc -l)
    NUMIM=$(cat ${TMPFILE} | wc -l)
    echo ${NUMIM}
}

validateParameters () {

    # Use current directory if no directory or files were passed
    if [ -z "${DIRECTORY}" ] ; then
        DIRECTORY="."
    fi

    createTempFile

    i=$(countImagesInDir)
    ${VERBOSE} && echo "Num images : ${i}"

    # Validate directory
    if [ -n "${DIRECTORY}" ] ; then
        [ $i -le 0 ] && VALID=false
        ! ${VALID}  &&
            echo "ERROR: Invalid directory name \"${DIRECTORY}\" or directory does not contain image files." &&
            return 1
    fi

    # Validate files
    if [ -n "${FILES}" ] ; then
        for FILE in "${FILES[@]}"
        do
            [ -f "${FILE}" ] && identify "${FILE}" &>> /dev/null 
            [ "$?" -ne 0 ] && VALID=false
            ! ${VALID} && 
                echo "ERROR: Invalid file name ${FILE} or file is not an image file." && 
                return 1
        done
    fi

    ${SINGLEIMG} && [ ${#FILES[@]} -gt 1 ] && echo "Warning single file parameter but two files specified"

    return 0
}

getScreenGeometry () {
    SCREENGEOMETRY=$(xrandr | grep "Screen 0: " | sed "s/.*current \([0-9]*\) x \([0-9]*\).*/\1x\2/")
}

getMonitorsGeometry () {
    local MONITOR
    SAVEIFS=$IFS
    IFS=$(echo -en "\n\b")
    MONITORS=()

    for MONITOR in $(xrandr --listmonitors | grep "^ [0-9]" | cut -s -d " " -f 4)
    do
        VARARRAY=($(echo ${MONITOR} | tr '/' '\n' | tr 'x' '\n' | tr '+' '\n'))
        MONITORS+=($(echo ${VARARRAY[0]}x${VARARRAY[2]} +${VARARRAY[4]}+${VARARRAY[5]}))
    done
    IFS=$SAVEIFS
    NUMMONITORS=${#MONITORS[@]}
}

# From the directory select as many random files as requested in the input parameter
selectRandomImages () {
    local NUMIMAGES=$1
#    FILES=($(ls "${DIRECTORY}"/*.@(${FORMATS}) 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))
#    FILES=($(ls "${DIRECTORY}"/*.jpg "${DIRECTORY}"/*.jpeg "${DIRECTORY}"/*.png) 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))
#    FILES=($(ls "${DIRECTORY}" 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))

#    FILES=($(ls "${DIRECTORY}" 2>/dev/null | sort -R | tail -n ${NUMIMAGES} ))

    createTempFile
    FILES=($(cat /tmp/alf_images.tmp | sort -R | tail -n ${NUMIMAGES} | sed "s/ /_s_p_a_c_e_/g"))
    i=0
    for FILE in "${FILES[@]}"
    do
        ${VERBOSE} && echo "FILE $i: ${FILE}"
        i=$((i+1))
    done
}

assembleBackgroundImage () {
    local MONITOR
    local THISFILE
    local TEMPIMG=$(mktemp --suffix=.jpg -p /tmp tmpXXX)
    local TEMPOUT=$(mktemp --suffix=.jpg -p /tmp tmpXXX)

    # Creates the blank base image
    convert -size ${SCREENGEOMETRY} xc:skyblue ${TEMPOUT}

    i=0
    for MONITOR in "${MONITORS[@]}"
    do
        j=0
        for FILE in "${FILES[@]}"
        do
            if [ $j -eq $i ] ; then
#                THISFILE="${FILE}"
                THISFILE=("$(echo ${FILE} | sed "s/_s_p_a_c_e_/ /g")")
#                ${VERBOSE} && echo "THISFILE $j: ${THISFILE}"
            fi
            j=$((j+1))
        done
        GEOMETRY=$(echo ${MONITOR} | cut -s -d " " -f 1)
        OFFSET=$(echo ${MONITOR} | cut -s -d " " -f 2)
#        ${VERBOSE} && echo "Monitor $i : ${GEOMETRY}${OFFSET} : ${FILES[$i]}"
        ${VERBOSE} && echo "Monitor $i : ${GEOMETRY}${OFFSET} : ${THISFILE}"
#        convert "${DIRECTORY}/${FILES[$i]}" -auto-orient -scale ${GEOMETRY}^ -gravity center -extent ${GEOMETRY} ${TEMPIMG}
#        convert "${DIRECTORY}/${FILES[$i]}" -resize ${GEOMETRY} -gravity center -background "rgb(0,0,0)" -extent ${GEOMETRY} ${TEMPIMG}
        convert "${DIRECTORY}/${THISFILE}" -resize ${GEOMETRY} -gravity center -background "rgb(0,0,0)" -extent ${GEOMETRY} ${TEMPIMG}
        composite -geometry ${OFFSET} ${TEMPIMG} ${TEMPOUT} ${TEMPOUT}
        i=$((i+1))
        [ $i -ge ${#FILES[@]} ] && i=0
    done
    rm "${TEMPIMG}"
    mv "${TEMPOUT}" "${OUTIMG}"
}

applyBackground () {
    gsettings set org.cinnamon.desktop.background picture-options "spanned"
    gsettings set org.cinnamon.desktop.background picture-uri "file://$(readlink -f ${OUTIMG})"
}

spanSingleImage () {
    [ -z "${FILES}" ] && selectRandomImages 1
    ${VERBOSE} && echo "File : ${FILES[0]}"

    convert "${FILES[0]}" -auto-orient -scale ${SCREENGEOMETRY}^ -gravity center -extent ${SCREENGEOMETRY} "${OUTIMG}"
}

assembleOneImagePerMonitor () {
    getMonitorsGeometry
    [ -z "${FILES}" ] && selectRandomImages ${NUMMONITORS}
    assembleBackgroundImage
}

setBackground () {
    local ORIGFILES=${FILES}
    getScreenGeometry
    if ${SINGLEIMG} ; then spanSingleImage 
    else assembleOneImagePerMonitor 
    fi
    [ -f "${OUTIMG}" ] && applyBackground
    FILES=${ORIGFILES}
}
#====== MAIN BODY OF THE SCRIPT ===

readParameters $@

if ${VALID} ; then
    if ${LOOP} ; then
        while true; do ${VERBOSE} && echo "Let's go"; setBackground ; sleep ${INTERVAL}; done
    else
        setBackground
    fi
else
    echo "Use -h for help."
    exit 1
fi
AlfredoLlaquet commented 2 years ago

Second version of my version. Now the script works on Plasma, XFCE and MATE besides Cinnamon.

#!/bin/bash

# This script creates an image to span multiple monitors under Cinnamon, MATE, Plasma and XFCE
# It receives only one parameter which can be
# - A single file name : The script resizes and shaves the image to fit the screeens (does it still work? not sure)
# - A directory with potential files: the script selects randomly one file per monitor.
#
#
#Requires:
#  ImageMagick
#  xrandr
#
# Author: Raul Suarez
# https://www.usingfoss.com/
#
# Much improved to his liking by: Alfredo Llaquet-Alsina
#
# License: GPL v3.0

#SCRIPTNAME=$(basename $_)
SCRIPTNAME=${0##*/}
#====== VALIDATE DEPENDENCIES ===

command -v gsettings >/dev/null 2>&1 || 
    { echo >&2 "This script only works on systems which rely on gsettings.  Aborting."; exit 1; }
command -v xrandr >/dev/null 2>&1 || 
    { echo >&2 "This script only works on systems which rely on xrandr.  Aborting."; exit 1; }
command -v identify >/dev/null 2>&1 || 
    { echo >&2 "Please install 'imagemagick'.  Aborting."; exit 1; }

#====== GLOBAL VARIABLES ===
VERSION=0.2.2
VALID=true
FORMATS="jpg|jpeg|png"
OUTIMG=${HOME}/.cinnamon/backgrounds/multiMonitorBackground.jpg

MONITORS=()
SCREENGEOMETRY=""
DIRECTORY=""
SINGLEIMG=false
VERBOSE=false
LOOP=false
INTERVAL=""
MYLIST=""
TMPFILE="/tmp/alf_images.tmp"

declare -a FILES=()

# Allow extended pattern substitutions
shopt -s extglob

#====== FUNCTIONS ===

showHelp () {
    echo "Usage: multiMonitorBackground [OPTIONS] [FILE] [FILE] ...

This script to set up a background image spaning multiple monitors 
under Cinnamon. If no parameters are specified: A random file per monitor is 
selected from current directory.

Note: Files are scaled and shaved to fit the display area without loosing aspect radio.

It can receive the following parameters:
 FILE          Files to set as background. FILE paths must be relative to the
               DIRECTORY unless they have an absolute path. Each FILE must be 
               an image file.
               - If the script receives less files than the number of monitors
                 it will cycle through the files repeating them until all
                 monitors have a file
               - If the script receives no files and the parameter -s is not 
                 specified the script will select one random image per active 
                 monitor
 -d DIRECTORY  A directory containing image files. The
               default is the current directory
 -s            Span a single FILE across monitors.  
               If no FILE is passed, and the parameter -s is present, a random
               file from the DIRECTORY is displayed.
 -t SECONDS    Time in SECONDS to refresh the background
 -version      Displays the version number
 -verbose      Displays additional information

Examples
: put the same image in each monitor
  ${SCRIPTNAME} mypic.jpg     
: span an image across all the monitors  
  ${SCRIPTNAME} mypic.jpg     
: select one random image per monitor from the directory indicated
  ${SCRIPTNAME} -d ~/Pics
: assign each image received to one or more monitors
  ${SCRIPTNAME} -d ~/Pics pic1.jpg pic2.jpg 

Requires:
  ImageMagick
  xrandr

Author: Raul Suarez
https://www.usingfoss.com/
"
}

readParameters () {
    DIRECTORY=""
    SINGLEIMG=false
    VERBOSE=false
    LOOP=false
    INTERVAL=""
    FILES=()

    while [ $# -gt 0 ] ; do
        unset OPTIND
        unset OPTARG
        while getopts :hsv:d:t:  options; do
            case $options in
                h)  showHelp
                    exit 0
                    ;;
                v)  local SECONDPART="$OPTARG"
                    [ "${SECONDPART}" != "ersion" ] && [ "${SECONDPART}" != "erbose" ] && 
                        echo "Invalid parameter -v$SECONDPART." && 
                        VALID=false
                    [ ${SECONDPART} == "ersion" ] && 
                        echo Version ${VERSION} && 
                        exit 0
                    [ ${SECONDPART} == "erbose" ] && 
                        VERBOSE=true
                    ;;
                d)  DIRECTORY="$OPTARG"
                    ;;
                s)  SINGLEIMG=true
                    ;;
                t)  INTERVAL=$((OPTARG))
                    LOOP=true
                    ;;
                :)  case $OPTARG in
                        v)  echo "Invalid parameter -v." >&2
                            ;;
                        t)  echo "Parameter -t usage -t <MINUTES>." >&2
                            ;;
                        d)  echo "Parameter -d usage -d <DIRECTORY>." >&2
                            ;;
                    esac
                    VALID=false
                    ;;
                ?)  echo "Invalid parameter -$OPTARG." >&2
                    VALID=false
                    ;;
            esac
        done
        shift $((OPTIND-1))
        FILES+=(${1})
        shift
    done

    [ -n "${DIRECTORY}" ] && [ -n "${FILES}" ] && assembleFullFileNames

    ${VERBOSE} && echo "Directory : ${DIRECTORY}"
    ${VERBOSE} && [ -n "${FILES}" ] && echo "Files : ${FILES[@]}"
    ${VERBOSE} && echo "Single image : ${SINGLEIMG}"
    ${VERBOSE} && ${LOOP} && echo "Refresh every : $((INTERVAL)) seconds"

    $VALID && validateParameters
    [ -z "${DIRECTORY}" ] && DIRECTORY="."
}

assembleFullFileNames () {

    declare -i i=0
    for FILE in "${FILES[@]}"
    do
        if [ "$(readlink -f ${FILE})" != "${FILE}" ]; then
            FILES[$i]="${DIRECTORY}/${FILES[$i]}"
        fi
        i+=1
    done
}

createTempFile () {
    if [ -z "${MYLIST}" ] || [ ! -f "${TMPFILE}" ] ; then
        ${VERBOSE} && echo "Creating temp list of files"
        MYLIST="done"
        ls "${DIRECTORY}" 2>/dev/null 1> "${TMPFILE}"
    fi
}

countImagesInDir () {
#    local TESTDIR="${1}"
#    echo $(ls "${TESTDIR}"/*.jpg "${TESTDIR}"/*.jpeg "${TESTDIR}"/*.png 2>/dev/null | wc -l)
# #     echo $(ls "${TESTDIR}"/*.@(${FORMATS}) 2>/dev/null | wc -l)
    NUMIM=$(cat ${TMPFILE} | wc -l)
    echo ${NUMIM}
}

validateParameters () {

    # Use current directory if no directory or files were passed
    if [ -z "${DIRECTORY}" ] ; then
        DIRECTORY="."
    fi

    createTempFile

    i=$(countImagesInDir)
    ${VERBOSE} && echo "Num images : ${i}"

    # Validate directory
    if [ -n "${DIRECTORY}" ] ; then
        [ $i -le 0 ] && VALID=false
        ! ${VALID}  &&
            echo "ERROR: Invalid directory name \"${DIRECTORY}\" or directory does not contain image files." &&
            return 1
    fi

    # Validate files
    if [ -n "${FILES}" ] ; then
        for FILE in "${FILES[@]}"
        do
            [ -f "${FILE}" ] && identify "${FILE}" &>> /dev/null 
            [ "$?" -ne 0 ] && VALID=false
            ! ${VALID} && 
                echo "ERROR: Invalid file name ${FILE} or file is not an image file." && 
                return 1
        done
    fi

    ${SINGLEIMG} && [ ${#FILES[@]} -gt 1 ] && echo "Warning single file parameter but two files specified"

    return 0
}

getScreenGeometry () {
    SCREENGEOMETRY=$(xrandr | grep "Screen 0: " | sed "s/.*current \([0-9]*\) x \([0-9]*\).*/\1x\2/")
}

getMonitorsGeometry () {
    local MONITOR
    SAVEIFS=$IFS
    IFS=$(echo -en "\n\b")
    MONITORS=()

    for MONITOR in $(xrandr --listmonitors | grep "^ [0-9]" | cut -s -d " " -f 4)
    do
        VARARRAY=($(echo ${MONITOR} | tr '/' '\n' | tr 'x' '\n' | tr '+' '\n'))
        MONITORS+=($(echo ${VARARRAY[0]}x${VARARRAY[2]} +${VARARRAY[4]}+${VARARRAY[5]}))
    done
    IFS=$SAVEIFS
    NUMMONITORS=${#MONITORS[@]}
}

# From the directory select as many random files as requested in the input parameter
selectRandomImages () {
    local NUMIMAGES=$1
#    FILES=($(ls "${DIRECTORY}"/*.@(${FORMATS}) 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))
#    FILES=($(ls "${DIRECTORY}"/*.jpg "${DIRECTORY}"/*.jpeg "${DIRECTORY}"/*.png) 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))
#    FILES=($(ls "${DIRECTORY}" 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))

#    FILES=($(ls "${DIRECTORY}" 2>/dev/null | sort -R | tail -n ${NUMIMAGES} ))

    createTempFile
    FILES=($(cat /tmp/alf_images.tmp | sort -R | tail -n ${NUMIMAGES} | sed "s/ /_s_p_a_c_e_/g"))
    i=0
    for FILE in "${FILES[@]}"
    do
        ${VERBOSE} && echo "FILE $i: ${FILE}"
        i=$((i+1))
    done
}

assembleBackgroundImage () {
    local MONITOR
    local THISFILE
    local TEMPIMG=$(mktemp --suffix=.jpg -p /tmp tmpXXX)
    local TEMPOUT=$(mktemp --suffix=.jpg -p /tmp tmpXXX)

    # Creates the blank base image
    convert -size ${SCREENGEOMETRY} xc:skyblue ${TEMPOUT}

    i=0
    for MONITOR in "${MONITORS[@]}"
    do
        ${VERBOSE} && echo "i: $i"
        j=0
        for FILE in "${FILES[@]}"
        do
            if [ $j -eq $i ] ; then
#                THISFILE="${FILE}"
                THISFILE=("$(echo ${FILE} | sed "s/_s_p_a_c_e_/ /g")")
                ${VERBOSE} && echo "THISFILE $j: ${THISFILE}"
            fi
            j=$((j+1))
        done

        if [ "${KDE_FULL_SESSION}" == "true" ]; then
            if command -v qdbus-qt5 &>/dev/null; then
                qdbus_command=qdbus-qt5
            else
                qdbus_command=qdbus
            fi
            QT_SELECT=5 $qdbus_command org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "
            var allDesktops = desktops();
            allDesktops[$i].wallpaperPlugin = 'org.kde.image';
            allDesktops[$i].currentConfigGroup = Array('Wallpaper', 'org.kde.image', 'General');
            allDesktops[$i].writeConfig('Image', 'file://""${DIRECTORY}/$THISFILE""');
            allDesktops[$i].writeConfig('FillMode', '1');
            "
        else
            GEOMETRY=$(echo ${MONITOR} | cut -s -d " " -f 1)
            OFFSET=$(echo ${MONITOR} | cut -s -d " " -f 2)
#        ${VERBOSE} && echo "Monitor $i : ${GEOMETRY}${OFFSET} : ${FILES[$i]}"
            ${VERBOSE} && echo "Monitor $i : ${GEOMETRY}${OFFSET} : ${THISFILE}"
#        convert "${DIRECTORY}/${FILES[$i]}" -auto-orient -scale ${GEOMETRY}^ -gravity center -extent ${GEOMETRY} ${TEMPIMG}
#        convert "${DIRECTORY}/${FILES[$i]}" -resize ${GEOMETRY} -gravity center -background "rgb(0,0,0)" -extent ${GEOMETRY} ${TEMPIMG}
            convert "${DIRECTORY}/${THISFILE}" -resize ${GEOMETRY} -gravity center -background "rgb(0,0,0)" -extent ${GEOMETRY} ${TEMPIMG}
            composite -geometry ${OFFSET} ${TEMPIMG} ${TEMPOUT} ${TEMPOUT}
        fi
        i=$((i+1))
        [ $i -ge ${#FILES[@]} ] && i=0
    done
    rm "${TEMPIMG}"
    mv "${TEMPOUT}" "${OUTIMG}"
}

applyBackground () {
    if [ "$desktop" == "Cinnamon" ] || [ "$XDG_CURRENT_DESKTOP" == "X-Cinnamon" ]; then
        gsettings set org.cinnamon.desktop.background picture-options "spanned"
        gsettings set org.cinnamon.desktop.background picture-uri "file://$(readlink -f ${OUTIMG})"
    elif [ "$XDG_CURRENT_DESKTOP" == "MATE" ]; then
        gsettings set org.mate.background picture-filename "$(readlink -f ${OUTIMG})" 2> /dev/null
    elif [ "$desktop" == "xubuntu" ] || [ "$XDG_CURRENT_DESKTOP" == "XFCE" ]; then
        rc=$?
        if [[ $rc = 0 ]] ; then
            for i in $(xfconf-query -c xfce4-desktop -p /backdrop -l | grep -E -e "screen.*/monitor.*image-path$" -e "screen.*/monitor.*/last-image$"); do
                xfconf-query -c xfce4-desktop -p "$i" -n -t string -s "" 2> /dev/null
                xfconf-query -c xfce4-desktop -p "$i" -s "" 2> /dev/null
                xfconf-query -c xfce4-desktop -p "$i" -s "$(readlink -f ${OUTIMG})" 2> /dev/null
            done
        fi
    fi
}

spanSingleImage () {
    [ -z "${FILES}" ] && selectRandomImages 1
    ${VERBOSE} && echo "File : ${FILES[0]}"

    convert "${FILES[0]}" -auto-orient -scale ${SCREENGEOMETRY}^ -gravity center -extent ${SCREENGEOMETRY} "${OUTIMG}"
}

assembleOneImagePerMonitor () {
    getMonitorsGeometry
    [ -z "${FILES}" ] && selectRandomImages ${NUMMONITORS}
    assembleBackgroundImage
}

setBackground () {
    local ORIGFILES=${FILES}
    getScreenGeometry
    if ${SINGLEIMG} ; then spanSingleImage 
    else assembleOneImagePerMonitor 
    fi
    [ -f "${OUTIMG}" ] && applyBackground
    FILES=${ORIGFILES}
}
#====== MAIN BODY OF THE SCRIPT ===

readParameters $@

if ${VALID} ; then
    if ${LOOP} ; then
        while true; do ${VERBOSE} && echo "Let's go"; setBackground ; sleep ${INTERVAL}; done
    else
        setBackground
    fi
else
    echo "Use -h for help."
    exit 1
fi
AlfredoLlaquet commented 1 year ago

Third version of my version. Now the script works on Plasma, XFCE, MATE and GNOME besides Cinnamon.

#!/bin/bash

# This script creates an image to span multiple monitors under Cinnamon
# It receives only one parameter which can be
# - A single file name : The script resizes and shaves the image to fit the screeens
# - A directory with potential files: the script selects randomly one file per monitor.
#
# Note: Files are scaled and shaved to fit the display area without loosing aspect radio.
#
#Requires:
#  ImageMagick
#  xrandr
#
# Author: Raul Suarez
# https://www.usingfoss.com/
#
# Improved to his liking by: Alfredo Llaquet Alsina
#
# License: GPL v3.0

#SCRIPTNAME=$(basename $_)
SCRIPTNAME=${0##*/}
#====== VALIDATE DEPENDENCIES ===

command -v gsettings >/dev/null 2>&1 || 
    { echo >&2 "This script only works on systems which rely on gsettings.  Aborting."; exit 1; }
command -v xrandr >/dev/null 2>&1 || 
    { echo >&2 "This script only works on systems which rely on xrandr.  Aborting."; exit 1; }
command -v identify >/dev/null 2>&1 || 
    { echo >&2 "Please install 'imagemagick'.  Aborting."; exit 1; }

#====== GLOBAL VARIABLES ===
VERSION=0.2.2
VALID=true
FORMATS="jpg|jpeg|png"
# OUTIMG=${HOME}/.cinnamon/backgrounds/multiMonitorBackground.jpg
OUTIMG=${HOME}/Pictures/multiMonitorBackground.jpg
# OUTIMG=/tmp/multiMonitorBackground.jpg

MONITORS=()
SCREENGEOMETRY=""
DIRECTORY=""
SINGLEIMG=false
VERBOSE=false
LOOP=false
INTERVAL=""
MYLIST=""
TMPFILE="/tmp/alf_images.tmp"

declare -a FILES=()

# Allow extended pattern substitutions
shopt -s extglob

#====== FUNCTIONS ===

showHelp () {
    echo "Usage: multiMonitorBackground [OPTIONS] [FILE] [FILE] ...

This script to set up a background image spaning multiple monitors 
under Cinnamon. If no parameters are specified: A random file per monitor is 
selected from current directory.

Note: Files are scaled and shaved to fit the display area without loosing aspect radio.

It can receive the following parameters:
 FILE          Files to set as background. FILE paths must be relative to the
               DIRECTORY unless they have an absolute path. Each FILE must be 
               an image file.
               - If the script receives less files than the number of monitors
                 it will cycle through the files repeating them until all
                 monitors have a file
               - If the script receives no files and the parameter -s is not 
                 specified the script will select one random image per active 
                 monitor
 -d DIRECTORY  A directory containing image files. The
               default is the current directory
 -s            Span a single FILE across monitors.  
               If no FILE is passed, and the parameter -s is present, a random
               file from the DIRECTORY is displayed.
 -t SECONDS    Time in SECONDS to refresh the background
 -version      Displays the version number
 -verbose      Displays additional information

Examples
: put the same image in each monitor
  ${SCRIPTNAME} mypic.jpg     
: span an image across all the monitors  
  ${SCRIPTNAME} mypic.jpg     
: select one random image per monitor from the directory indicated
  ${SCRIPTNAME} -d ~/Pics
: assign each image received to one or more monitors
  ${SCRIPTNAME} -d ~/Pics pic1.jpg pic2.jpg 

Requires:
  ImageMagick
  xrandr

Author: Raul Suarez
https://www.usingfoss.com/
"
}

readParameters () {
    DIRECTORY=""
    SINGLEIMG=false
    VERBOSE=false
    LOOP=false
    INTERVAL=""
    FILES=()

    while [ $# -gt 0 ] ; do
        unset OPTIND
        unset OPTARG
        while getopts :hsv:d:t:  options; do
            case $options in
                h)  showHelp
                    exit 0
                    ;;
                v)  local SECONDPART="$OPTARG"
                    [ "${SECONDPART}" != "ersion" ] && [ "${SECONDPART}" != "erbose" ] && 
                        echo "Invalid parameter -v$SECONDPART." && 
                        VALID=false
                    [ ${SECONDPART} == "ersion" ] && 
                        echo Version ${VERSION} && 
                        exit 0
                    [ ${SECONDPART} == "erbose" ] && 
                        VERBOSE=true
                    ;;
                d)  DIRECTORY="$OPTARG"
                    ;;
                s)  SINGLEIMG=true
                    ;;
                t)  INTERVAL=$((OPTARG))
                    LOOP=true
                    ;;
                :)  case $OPTARG in
                        v)  echo "Invalid parameter -v." >&2
                            ;;
                        t)  echo "Parameter -t usage -t <MINUTES>." >&2
                            ;;
                        d)  echo "Parameter -d usage -d <DIRECTORY>." >&2
                            ;;
                    esac
                    VALID=false
                    ;;
                ?)  echo "Invalid parameter -$OPTARG." >&2
                    VALID=false
                    ;;
            esac
        done
        shift $((OPTIND-1))
        FILES+=(${1})
        shift
    done

    [ -n "${DIRECTORY}" ] && [ -n "${FILES}" ] && assembleFullFileNames

    ${VERBOSE} && echo "Directory : ${DIRECTORY}"
    ${VERBOSE} && [ -n "${FILES}" ] && echo "Files : ${FILES[@]}"
    ${VERBOSE} && echo "Single image : ${SINGLEIMG}"
    ${VERBOSE} && ${LOOP} && echo "Refresh every : $((INTERVAL)) seconds"

    $VALID && validateParameters
    [ -z "${DIRECTORY}" ] && DIRECTORY="."
}

assembleFullFileNames () {
    ${VERBOSE} && echo "assembleFullFileNames"

    declare -i i=0
    for FILE in "${FILES[@]}"
    do
        if [ "$(readlink -f ${FILE})" != "${FILE}" ]; then
            FILES[$i]="${DIRECTORY}/${FILES[$i]}"
        fi
        i+=1
    done
}

createTempFile () {
    ${VERBOSE} && echo "createTempFile"
    if [ -z "${MYLIST}" ] || [ ! -f "${TMPFILE}" ] ; then
        ${VERBOSE} && echo "Creating temp list of files"
        MYLIST="done"
        ls "${DIRECTORY}" 2>/dev/null 1> "${TMPFILE}"
    fi
}

countImagesInDir () {
    ${VERBOSE} && echo "countImagesInDir"
#    local TESTDIR="${1}"
#    echo $(ls "${TESTDIR}"/*.jpg "${TESTDIR}"/*.jpeg "${TESTDIR}"/*.png 2>/dev/null | wc -l)
# #     echo $(ls "${TESTDIR}"/*.@(${FORMATS}) 2>/dev/null | wc -l)
    NUMIM=$(cat ${TMPFILE} | wc -l)
    echo ${NUMIM}
}

validateParameters () {
    ${VERBOSE} && echo "validateParameters"

    # Use current directory if no directory or files were passed
    if [ -z "${DIRECTORY}" ] ; then
        DIRECTORY="."
    fi

    createTempFile

    i=$(countImagesInDir)
    ${VERBOSE} && echo "Num images : ${i}"

    # Validate directory
    if [ -n "${DIRECTORY}" ] ; then
        [ $i -le 0 ] && VALID=false
        ! ${VALID}  &&
            echo "ERROR: Invalid directory name \"${DIRECTORY}\" or directory does not contain image files." &&
            return 1
    fi

    # Validate files
    if [ -n "${FILES}" ] ; then
        for FILE in "${FILES[@]}"
        do
            [ -f "${FILE}" ] && identify "${FILE}" &>> /dev/null 
            [ "$?" -ne 0 ] && VALID=false
            ! ${VALID} && 
                echo "ERROR: Invalid file name ${FILE} or file is not an image file." && 
                return 1
        done
    fi

    ${SINGLEIMG} && [ ${#FILES[@]} -gt 1 ] && echo "Warning single file parameter but two files specified"

    return 0
}

getScreenGeometry () {
    ${VERBOSE} && echo "getScreenGeometry"
    SCREENGEOMETRY=$(xrandr | grep "Screen 0: " | sed "s/.*current \([0-9]*\) x \([0-9]*\).*/\1x\2/")
}

getMonitorsGeometry () {
    ${VERBOSE} && echo "getMonitorsGeometry"
    local MONITOR
    SAVEIFS=$IFS
    IFS=$(echo -en "\n\b")
    MONITORS=()

    for MONITOR in $(xrandr --listmonitors | grep "^ [0-9]" | cut -s -d " " -f 4)
    do
        VARARRAY=($(echo ${MONITOR} | tr '/' '\n' | tr 'x' '\n' | tr '+' '\n'))
        MONITORS+=($(echo ${VARARRAY[0]}x${VARARRAY[2]} +${VARARRAY[4]}+${VARARRAY[5]}))
    done
    IFS=$SAVEIFS
    NUMMONITORS=${#MONITORS[@]}
}

# From the directory select as many random files as requested in the input parameter
selectRandomImages () {
    ${VERBOSE} && echo "selectRandomImages"
    local NUMIMAGES=$1
#    FILES=($(ls "${DIRECTORY}"/*.@(${FORMATS}) 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))
#    FILES=($(ls "${DIRECTORY}"/*.jpg "${DIRECTORY}"/*.jpeg "${DIRECTORY}"/*.png) 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))
#    FILES=($(ls "${DIRECTORY}" 2>/dev/null | sort -R | tail -n ${NUMIMAGES} | sed "s/\n/ /"))

#    FILES=($(ls "${DIRECTORY}" 2>/dev/null | sort -R | tail -n ${NUMIMAGES} ))

    createTempFile
    FILES=($(cat /tmp/alf_images.tmp | sort -R | tail -n ${NUMIMAGES} | sed "s/ /_s_p_a_c_e_/g"))
    i=0
    for FILE in "${FILES[@]}"
    do
        ${VERBOSE} && echo "FILE $i: ${FILE}"
        i=$((i+1))
    done
}

assembleBackgroundImage () {
    ${VERBOSE} && echo "assembleBackgroundImage"
    local MONITOR
    local THISFILE
    local TEMPIMG=$(mktemp --suffix=.jpg -p /tmp tmpXXX)
    local TEMPOUT=$(mktemp --suffix=.jpg -p /tmp tmpXXX)

    # Creates the blank base image
    convert -size ${SCREENGEOMETRY} xc:skyblue ${TEMPOUT}

    i=0
    for MONITOR in "${MONITORS[@]}"
    do
        ${VERBOSE} && echo "i: $i"
        j=0
        for FILE in "${FILES[@]}"
        do
            if [ $j -eq $i ] ; then
#                THISFILE="${FILE}"
                THISFILE=("$(echo ${FILE} | sed "s/_s_p_a_c_e_/ /g")")
                ${VERBOSE} && echo "THISFILE $j: ${THISFILE}"
            fi
            j=$((j+1))
        done

        if [ "${KDE_FULL_SESSION}" == "true" ]; then
            if command -v qdbus-qt5 &>/dev/null; then
                qdbus_command=qdbus-qt5
            else
                qdbus_command=qdbus
            fi
            QT_SELECT=5 $qdbus_command org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "
            var allDesktops = desktops();
            allDesktops[$i].wallpaperPlugin = 'org.kde.image';
            allDesktops[$i].currentConfigGroup = Array('Wallpaper', 'org.kde.image', 'General');
            allDesktops[$i].writeConfig('Image', 'file://""${DIRECTORY}/$THISFILE""');
            allDesktops[$i].writeConfig('FillMode', '1');
            "
        else
            GEOMETRY=$(echo ${MONITOR} | cut -s -d " " -f 1)
            OFFSET=$(echo ${MONITOR} | cut -s -d " " -f 2)
#        ${VERBOSE} && echo "Monitor $i : ${GEOMETRY}${OFFSET} : ${FILES[$i]}"
            ${VERBOSE} && echo "Monitor $i : ${GEOMETRY}${OFFSET} : ${THISFILE}"
#        convert "${DIRECTORY}/${FILES[$i]}" -auto-orient -scale ${GEOMETRY}^ -gravity center -extent ${GEOMETRY} ${TEMPIMG}
#        convert "${DIRECTORY}/${FILES[$i]}" -resize ${GEOMETRY} -gravity center -background "rgb(0,0,0)" -extent ${GEOMETRY} ${TEMPIMG}
            convert "${DIRECTORY}/${THISFILE}" -resize ${GEOMETRY} -gravity center -background "rgb(0,0,0)" -extent ${GEOMETRY} ${TEMPIMG}
            composite -geometry ${OFFSET} ${TEMPIMG} ${TEMPOUT} ${TEMPOUT}
        fi
        i=$((i+1))
        [ $i -ge ${#FILES[@]} ] && i=0
    done
    rm "${TEMPIMG}"
    mv "${TEMPOUT}" "${OUTIMG}"
}

applyBackground () {
    ${VERBOSE} && echo "applyBakcground"
    ${VERBOSE} && echo "\$desktop : $desktop"
    ${VERBOSE} && echo "\$XDG_CURRENT_DESKTOP : $XDG_CURRENT_DESKTOP"
    if [ "$desktop" == "Cinnamon" ] || [ "$XDG_CURRENT_DESKTOP" == "X-Cinnamon" ]; then
        gsettings set org.cinnamon.desktop.background picture-options "spanned"
        gsettings set org.cinnamon.desktop.background picture-uri "file://$(readlink -f ${OUTIMG})"
    elif [ "$XDG_CURRENT_DESKTOP" == "GNOME" ]; then
        gsettings set org.gnome.desktop.background picture-options "spanned"
        gsettings set org.gnome.desktop.background picture-uri "file://$(readlink -f ${OUTIMG})"
        gsettings set org.gnome.desktop.background picture-uri-dark "file://$(readlink -f ${OUTIMG})"
    elif [ "$XDG_CURRENT_DESKTOP" == "MATE" ]; then
        gsettings set org.mate.background picture-filename "$(readlink -f ${OUTIMG})" 2> /dev/null
    elif [ "$desktop" == "xubuntu" ] || [ "$XDG_CURRENT_DESKTOP" == "XFCE" ]; then
        rc=$?
        if [[ $rc = 0 ]] ; then
            for i in $(xfconf-query -c xfce4-desktop -p /backdrop -l | grep -E -e "screen.*/monitor.*image-path$" -e "screen.*/monitor.*/last-image$"); do
                xfconf-query -c xfce4-desktop -p "$i" -n -t string -s "" 2> /dev/null
                xfconf-query -c xfce4-desktop -p "$i" -s "" 2> /dev/null
                xfconf-query -c xfce4-desktop -p "$i" -s "$(readlink -f ${OUTIMG})" 2> /dev/null
            done
        fi
    fi
}

spanSingleImage () {
    ${VERBOSE} && echo "spanSingleImage"
    [ -z "${FILES}" ] && selectRandomImages 1
    ${VERBOSE} && echo "File : ${FILES[0]}"

    convert "${FILES[0]}" -auto-orient -scale ${SCREENGEOMETRY}^ -gravity center -extent ${SCREENGEOMETRY} "${OUTIMG}"
}

assembleOneImagePerMonitor () {
    ${VERBOSE} && echo "assembleOneImagePerMonitor"
    getMonitorsGeometry
    [ -z "${FILES}" ] && selectRandomImages ${NUMMONITORS}
    assembleBackgroundImage
}

setBackground () {
    ${VERBOSE} && echo "setBackground"
    local ORIGFILES=${FILES}
    getScreenGeometry
    if ${SINGLEIMG} ; then spanSingleImage 
    else assembleOneImagePerMonitor 
    fi
    [ -f "${OUTIMG}" ] && applyBackground
    FILES=${ORIGFILES}
}
#====== MAIN BODY OF THE SCRIPT ===

readParameters $@

if ${VALID} ; then
    if ${LOOP} ; then
        while true; do ${VERBOSE} && echo "Let's go"; setBackground ; sleep ${INTERVAL}; done
    else
        setBackground
    fi
else
    echo "Use -h for help."
    exit 1
fi